Cubis Engineers

Deploying applications

Ship a containerized web application behind Nginx and TLS with explicit release and rollback procedures.

Application deliveryAdvancedUpdated Aug 13, 2026dockercomposenginxtlsdeployment

This guide uses Docker Compose for the application stack and host-managed Nginx for the public edge. The same boundaries apply if you run the app directly with systemd.

Install Docker from its official repository

Use the current instructions for your distribution and verify the repository fingerprint before installation. Afterward:

Terminal
docker version
docker compose version
sudo systemctl enable --now docker
sudo usermod -aG docker deploy

Log out and back in for group membership to refresh. Membership in the docker group is effectively root-equivalent; grant it only to trusted operators.

Build a small, non-root image

Dockerfile
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --omit=dev

FROM node:22-alpine AS runtime
ENV NODE_ENV=production
WORKDIR /app
USER node
COPY --chown=node:node --from=build /app ./
EXPOSE 3000
CMD ["node", "server.js"]

Pin a known runtime major or immutable digest, exclude secrets with .dockerignore, and scan the final image in CI.

Define the runtime

compose.yaml
services:
  app:
    image: registry.example.com/cubis-api:${APP_VERSION}
    restart: unless-stopped
    env_file: /etc/cubis-api.env
    ports:
      - "127.0.0.1:3000:3000"
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp

Avoid latest. Deploy an immutable commit SHA or release version so the running artifact and rollback target are unambiguous.

Terminal
export APP_VERSION=2026.08.13-3f28c1a
docker compose pull
docker compose config --quiet
docker compose up -d --remove-orphans
docker compose ps
docker compose logs --tail=100 app

Put Nginx in front

/etc/nginx/sites-available/cubis-api
server {
    listen 80;
    listen [::]:80;
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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_connect_timeout 5s;
        proxy_read_timeout 60s;
    }
}
Terminal
sudo ln -s /etc/nginx/sites-available/cubis-api /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
curl -I -H 'Host: api.example.com' http://127.0.0.1

Enable TLS

Point DNS at the server first, allow ports 80 and 443, then use your organization’s certificate automation. With Certbot on Ubuntu:

Terminal
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d api.example.com
sudo certbot renew --dry-run
systemctl list-timers | grep certbot

Release and rollback

Record the intended version

Link the change, image digest, migration plan, owner, verification, and rollback target.

Pull before changing runtime

docker compose pull makes registry failures happen before the current containers are replaced.

Treat database migrations separately

Make database changes in stages: add the new structure, move the data, and remove the old structure only after every running application version has stopped using it. Know whether the migration can be reversed before deploying it.

Start and verify

Check container health, local health endpoint, public HTTPS, key user flow, and logs.

Roll back deliberately

Set APP_VERSION to the last known-good immutable version and run docker compose up -d. Verify with the same checklist.

A single container will briefly stop

A single container must release its port before the replacement can use it. To avoid that interruption, run at least two healthy instances behind a load balancer, add readiness checks, and keep database changes compatible with both application versions.

On this page