On this page
Two Node.js applications can share a server without sharing dependencies, release schedules, or public ports. Containers provide packaging and process isolation, while a reverse proxy gives each application its own hostname.
This guide builds a small Docker Compose deployment with separate application networks, persistent-data planning, and a controlled update workflow. The example assumes an Ubuntu-style server with Docker Engine and the Compose plugin already installed.
Plan addresses and exposure first
Before writing Compose files, list your server's LAN, VPN, and existing container ranges. Overlapping subnets can route traffic to the wrong interface and make otherwise healthy services appear unreachable.
Use the Subnet & CIDR Calculator below to check whether an intended container range overlaps addresses you already use. Enter 172.28.10.0/24, inspect its boundaries, and test representative addresses from your other networks. The calculator checks membership; compare every existing CIDR's boundaries rather than assuming one sample proves two ranges never overlap.
This example reserves 172.28.10.0/24 and 172.28.11.0/24 for two application networks. They are private address space, not universally safe defaults. Replace them when your own inventory shows a conflict. RFC 1918 defines private IPv4 ranges, but private does not mean unique across organizations or VPNs.
Only the reverse proxy needs published web ports. Databases, caches, and application listeners should remain unpublished unless there is a specific access requirement.
Package an application for predictable startup
Build an image from a known Node.js release compatible with your application. Keep dependency installation reproducible with a committed lockfile. This Dockerfile assumes a JavaScript app with server.js, a startable HTTP listener, and no build step; TypeScript or bundled projects need a separate build stage.
FROM node:24-bookworm-slim
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --chown=node:node . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]Add node_modules, .git, .env, local credentials, and unnecessary uploads to a .dockerignore file before building. Otherwise the final copy can overwrite image dependencies or embed secrets. For production release tracking, pin an approved image digest and update it intentionally when applying security fixes.
Your application must listen on 0.0.0.0:3000 inside the container, not just its own loopback interface. Handle SIGTERM by stopping new requests and closing database connections. Writing logs to standard output keeps them available through Docker's logging system rather than hidden inside an ephemeral filesystem.
Avoid baking configuration secrets into image layers. Runtime-mounted secrets or a dedicated secret manager are preferable, and applications must explicitly read the selected secret source. An environment variable is configuration delivery, not encryption.
Connect two applications through one proxy
Create two application directories, app-a and app-b, containing their Dockerfiles and source. Place the following compose.yaml beside them. Separate networks prevent the two applications from directly discovering one another; the proxy intentionally joins both networks.
services:
app_a:
build: ./app-a
init: true
restart: unless-stopped
mem_limit: 384m
cpus: 0.75
networks: [a_net]
app_b:
build: ./app-b
init: true
restart: unless-stopped
mem_limit: 384m
cpus: 0.75
networks: [b_net]
proxy:
image: nginx:stable
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks: [a_net, b_net]
networks:
a_net:
ipam:
config:
- subnet: 172.28.10.0/24
b_net:
ipam:
config:
- subnet: 172.28.11.0/24These resource ceilings are illustrative starting points, not capacity recommendations. Measure resident memory, CPU saturation, and response times before adjusting them. Leave room for the OS, proxy, filesystem cache, and deployment builds. A hard memory limit can terminate a process instead of slowing it gracefully.
Compose creates user-defined bridge networks with outbound connectivity by default; these are not air-gapped networks. Review the Docker documentation for network behavior and published-port firewall interactions. Do not assume a familiar host firewall rule automatically blocks every Docker-published port.
Route hostnames and add HTTPS deliberately
Save this as nginx.conf. The proxy reaches each application by its Compose service name, so fixed container IPs are unnecessary. Replace the hostnames with names you control and point their DNS records to the server.
server {
listen 80;
server_name a.example.com;
location / {
proxy_pass http://app_a:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name b.example.com;
location / {
proxy_pass http://app_b:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}This is an HTTP-only rehearsal, not a complete public deployment. Before accepting credentials or personal data, add certificate issuance, persistent certificate storage, port 443, and HTTPS server blocks using a maintained proxy workflow. The NGINX documentation explains proxy directives and TLS configuration.
Set framework proxy-trust settings narrowly for this topology. Trusting arbitrary forwarded headers from any source can corrupt client-IP detection or HTTPS redirects. If a CDN or additional load balancer sits in front, explicitly revise the trusted-proxy chain rather than retaining assumptions from the single-proxy example.
For certificate planning, the wildcard SSL automation guide covers DNS-based issuance. WebSocket applications additionally need appropriate upgrade handling; the basic HTTP configuration above does not implement it.
Deploy, inspect, and roll back safely
Validate the file with docker compose config, then run docker compose up -d --build. Inspect docker compose ps and docker compose logs --tail=100 before testing each hostname. Use docker compose exec proxy nginx -t to check the active proxy configuration.
Configure log rotation as well: container logs can exhaust the host filesystem even when application memory and CPU remain comfortably within their limits.
Keep persistent state outside disposable container layers. Database volumes and uploaded files need backups with tested restores; a saved application image is not a database backup. Avoid copying live database files without an engine-supported consistent-backup method.
For a controlled release, record the current image identifier, build or pull the next release, and test its health endpoint and core workflows. Plain Compose recreation can cause a short interruption and is not automatically a zero-downtime rollout. After replacing an application container, reload or recreate the proxy so its upstream name resolves to the current container address.
Watch docker stats during normal traffic and during builds. If one application repeatedly reaches its memory limit, investigate leaks, concurrency, and caching before increasing capacity. The website audit workflow helps separate infrastructure pressure from frontend problems.
Recheck network membership with the CIDR planning tool whenever adding a VPN, database network, or new environment. Rollback readiness means retaining the previous image and ensuring database migrations remain compatible; restoring an older container cannot undo an incompatible schema change safely.
Key takeaways
- Plan nonoverlapping networks before launching containers, especially on servers connected to VPNs.
- Publish proxy ports only, keep application dependencies reproducible, and run application processes without root privileges.
- Add HTTPS and backups before treating the rehearsal as production infrastructure.
- Measure resource usage, refresh proxy resolution after replacements, and retain a tested rollback path.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor