A Claude-powered application that works on a laptop is not production-ready merely because it starts in Docker. A reliable deployment also needs a reproducible image, protected credentials, a restricted network boundary, HTTPS, a restart policy, health checks, logs, and a rollback path.
This guide deploys a small Python web application to one Linux VPS with Docker Compose, Nginx, and an automated TLS certificate. The pattern suits an app created with Claude Code as well as an app that calls the Claude API. Adapt the image name, application port, health endpoint, and runtime command to your own project.
Define the Production Boundary First
The public internet should reach Nginx on ports 80 and 443. Nginx should proxy requests to the application on the VPS loopback interface. The application container does not need a public port, and the Docker daemon must not be exposed to the network.
- Public: DNS, HTTP for certificate validation and redirect, and HTTPS for the application.
- Private: the application port bound to 127.0.0.1, container networks, environment files, and the Docker socket.
- Persistent: only data that must survive container replacement, such as a database volume or uploaded files.
- Observable: a lightweight health endpoint, structured application logs, and host resource metrics.
Docker warns that published container ports can interact with host firewall rules in ways that surprise operators. Binding the app to 127.0.0.1 and exposing only the reverse proxy reduces the public surface. Docker’s packet-filtering documentation explains the firewall behavior in detail.
Build a Reproducible Application Image
Build the same container image in development and production. Do not copy a secret into the image, and do not rely on files that exist only on the developer’s laptop. A small Python example can start from an official slim image and run as a non-root user.
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
RUN addgroup --system app && adduser --system --ingroup app app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=app:app . .
USER app
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Create a .dockerignore file that excludes .env, local virtual environments, Git metadata, test caches, and other unnecessary files. Then build and test the image locally:
docker build -t claude-app:2026-08-19 . docker run --rm --env-file .env -p 127.0.0.1:8000:8000 claude-app:2026-08-19 curl --fail http://127.0.0.1:8000/health
Use a versioned tag or, preferably, an image digest for production. A mutable latest tag does not identify the bytes you tested and makes rollback evidence weaker.

Prepare the VPS and Install Docker Correctly
Choose a supported 64-bit Linux release. Ubuntu 24.04 LTS is a conservative baseline for this walkthrough. Docker’s current Ubuntu installation page also lists newer supported releases, so confirm the exact compatibility before provisioning.
Use the official Docker apt repository instructions rather than an unreviewed one-line installer. Install Docker Engine, Buildx, and the Compose plugin, then verify the service:
docker --version docker compose version sudo systemctl is-active docker sudo docker run --rm hello-world
Only trusted administrators should control Docker. Membership in the docker group is effectively privileged because the daemon can start containers with host access. Docker’s security guidance recommends reducing container privileges and limiting daemon access; rootless mode is also available when its operational tradeoffs fit the application.
For a small API-backed app, begin with measured requirements rather than a universal sizing rule. Watch memory during concurrent requests, account for Nginx and the operating system, and leave headroom for image pulls and deployments. If you need a KVM host with multiple regions and NVMe-backed options, compare HostStage unmanaged Linux VPS against the workload you observed.
Protect the Claude API Credential and Private Port
Anthropic’s authentication documentation says its SDKs can read ANTHROPIC_API_KEY from the environment. The same guidance recommends storing keys in a secrets manager, rotating them, and revoking a suspected leak. Never place a real key in a Dockerfile, image layer, repository, screenshot, or shell history.
On a single VPS, an owner-readable environment file is a practical minimum. Create it outside the repository, restrict its permissions, and reference it from Compose. A deployment platform with a proper secret store is stronger; Docker Compose can also mount secrets as read-only files when the application supports file-based credentials.
sudo install -d -m 0750 -o deploy -g deploy /opt/claude-app sudo install -m 0600 -o deploy -g deploy /dev/null /opt/claude-app/app.env sudoedit /opt/claude-app/app.env
Place only the required variables in that file. Do not print it during verification. Rotate the key after any accidental exposure, even if the value was quickly removed from a repository.

Deploy With Docker Compose
Compose records the runtime contract in a reviewable file. The following example binds the application to loopback, starts it after a reboot, adds a health check, drops unnecessary Linux capabilities, and limits log-file growth.
services:
web:
image: registry.example.com/claude-app:2026-08-19
env_file:
- /opt/claude-app/app.env
ports:
- "127.0.0.1:8000:8000"
restart: unless-stopped
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
The read-only filesystem and dropped capabilities require the application to write only to an explicit volume or /tmp. Remove a hardening option only when the app genuinely needs the capability, and document why.
cd /opt/claude-app docker compose config --quiet docker compose pull docker compose up -d docker compose ps curl --fail http://127.0.0.1:8000/health
Docker documents a separate production Compose file as one way to isolate production settings such as restart behavior, ports, logging, and environment configuration. Review the Compose production guidance when development and production need different overrides.
Put Nginx and TLS in Front of the App
Create an A or AAAA record for the application hostname, wait for it to resolve to the VPS, and allow inbound TCP 80 and 443. Keep port 8000 closed publicly because Nginx reaches it through loopback.
NGINX’s proxy module documentation shows proxy_pass and the request headers normally forwarded to an upstream. A minimal HTTP virtual host can start like this:
server {
listen 80;
listen [::]:80;
server_name ai.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
}
Validate before reloading: sudo nginx -t, followed by sudo systemctl reload nginx. Then use Certbot or another ACME client from its official installation channel to request a certificate and configure automatic renewal. Test renewal with sudo certbot renew –dry-run. A certificate is not a one-time task; renewal must remain monitored.

Make Updates and Rollbacks Deliberate
Before an update, record the running image digest and confirm that backups cover every persistent volume. Pull the new version, recreate the service, run the health check, and inspect logs. Do not assume a successful image pull means the application is healthy.
docker compose images docker compose pull docker compose up -d docker compose ps docker compose logs --since 10m web curl --fail https://ai.example.com/health
If verification fails, restore the previous versioned tag or digest in the Compose file and recreate the service. Database migrations need their own tested backward-compatibility and restore plan; changing the container image cannot reverse an incompatible data migration.
For related deployment and automation ideas, see HostStage’s guides to AI agents for small businesses and starting an AI content agency. Treat each workload as a separate capacity and security decision rather than copying one server size into every project.
Monitor the Service You Actually Run
There is no universal healthy CPU or memory percentage for a Claude app. Establish a baseline under representative traffic, then alert on sustained deviation, health-check failure, error rate, latency, disk pressure, certificate expiry, and repeated container restarts.
- Verify the public HTTPS endpoint and the private loopback health endpoint separately.
- Track request latency and errors at Nginx and inside the application.
- Watch memory working set, CPU saturation, disk usage, and image-storage growth.
- Alert when the container becomes unhealthy or restarts repeatedly.
- Review Claude API timeouts, rate-limit responses, and spend controls without logging prompts or credentials unnecessarily.
- Test backups and certificate renewal on a schedule rather than waiting for an incident.
A production check should prove the expected behavior: DNS resolves correctly, HTTP redirects to HTTPS, the certificate matches the hostname, the app survives a container restart, the secret is absent from the image history, and rollback instructions are current.

FAQ
Can a Claude app be deployed on one VPS?
Yes. A single VPS is a reasonable starting point for a small application when its availability, data, and scaling requirements fit one host. It remains one failure domain, so maintain backups, monitoring, and a recovery plan.
Should the Claude API key be copied into the Docker image?
No. Supply the credential at runtime from a protected environment file or secret manager. Keep it out of the Dockerfile, build context, repository, screenshots, and logs, and rotate it if exposure is suspected.
Why bind the application port to 127.0.0.1?
It prevents the published application port from listening on every host interface. Nginx can still reach the app locally while public clients use the controlled HTTP and HTTPS entry points.
Is a Docker restart policy enough for high availability?
No. A restart policy can recover a stopped process after some failures, but it cannot repair a failed VPS, corrupted data, broken deployment, exhausted disk, or unavailable upstream service.
How should production images be tagged?
Use an immutable digest or a unique version tag tied to the tested build. Record the previous digest so a rollback restores known bytes rather than whatever a mutable tag happens to reference later.
