Build a Resilient Local Web Platform

Build a resilient local web platform with Docker Compose and nginx.

Introduction

30 Second Summary

A web service can look reliable while its only server is running. The real test begins when that server disappears during a request.

In this project, you will use Docker Compose to build a local platform with two nginx web nodes behind one reverse proxy. You will first expose a single-node outage before proving the redesigned platform survives the same failure.

What You'll Build

You'll watch repeated requests to one local address alternate between served-by-web and served-by-web2.

By the end of this project, you'll have:

  • A round-robin traffic demo where repeated requests visibly alternate between two web nodes.
  • A segmented network boundary that exposes one local entry point while keeping both backend ports hidden from the host.
  • A controlled failover test that keeps returning successful responses after one backend stops. You'll support that result with health checks, Docker events, and proxy logs.
  • Secret Mission: Give web more routing weight than web2 and prove that it receives a larger share of requests.

Are there any prerequisites?

Docker Engine, Docker Compose, and VS Code must already be available on your Linux machine. Basic comfort with Linux commands and networking keeps the focus on architecture.

Before We Start

This project begins with a visible outage so you can prove what resilience changes. Before the hands-on work starts, commit to building a local platform that keeps serving requests through a surviving backend when another backend becomes unavailable.

Verify Your Local Toolchain

Your resilient platform depends on Docker Engine to run containers. Docker Compose will coordinate the services later.

A stopped Docker daemon or missing Compose plugin can resemble an architecture failure. You will verify both layers before creating any services.

In this step, get ready to:
  • Create the workspace structure in VS Code.
  • Confirm the Docker client can reach Docker Engine.
  • Confirm the Docker Compose plugin is available.
Create the workspace folders

A predictable folder layout gives each service its own configuration location. VS Code keeps that layout visible while you work.

  • Use your Linux application launcher to open the file manager.
  • Select the Desktop location in the file manager sidebar.
  • Click the new-folder control in the file manager toolbar.
  • Type resilient-web into the folder name field.
  • Press Enter to create the folder.

You should see the empty resilient-web folder on your Desktop.

  • Use your Linux application launcher to search for VS Code.
  • Select VS Code from the search results.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the resilient-web folder on your Desktop.
  • Click Open.

You should see resilient-web at the top of the Explorer sidebar.

  • Click the New Folder control in the Explorer sidebar.
  • Type proxy into the folder name field.
  • Press Enter.

You should see the empty proxy folder inside resilient-web.

  • Click the New Folder control in the Explorer sidebar.
  • Type web into the folder name field.
  • Press Enter.

You should see the empty web folder beside proxy.

  • Click the New Folder control in the Explorer sidebar.
  • Type web2 into the folder name field.
  • Press Enter.

Your Explorer sidebar should now show the three empty folders proxy, web, and web2 inside resilient-web.

Confirm Docker Engine is reachable

The Docker client sends instructions to Docker Engine. A version check proves that the client can communicate with the running server.

  • Click Terminal in the VS Code top menu.
  • Click New Terminal.
  • Check the Docker client and server connection by running:
docker version

What does this check?

  • The client information confirms that the Docker command-line tool is available.
  • The server information confirms that Docker Engine received the request.

You should see separate client and server information in the terminal. That result proves Docker Engine is reachable.

Missing the server information?

  • Confirm the Docker daemon is running with the service controls for your Linux distribution.
  • Repeat the version check after starting the daemon.

Need help with your environment? Help me diagnose why Docker Engine is unreachable on Linux.

Confirm Docker Compose is available

The Compose plugin turns a multi-service definition into a running application. This check confirms that Docker can access the plugin before you create the platform definition.

Before you run this check, do you expect Compose to report its own version or repeat the Docker Engine details?

  • Check the Compose plugin by running:
docker compose version

What does this check?

This command asks the installed Compose plugin for its version information. A version response confirms that Docker can access the plugin.

You should see Docker Compose version information below the earlier Docker client and server details. Your local container toolchain is ready for the platform build.

No Compose version information?

  • Check that your Docker installation includes the Compose plugin.
  • Follow the Docker package instructions for your Linux distribution if the plugin is missing.

Need a hand with the plugin? Help me diagnose why Docker Compose is unavailable.

Your workspace is structured for three services. Next up, you will launch one web node and stop it on purpose to expose the platform's first failure domain.

Launch and Break One Web Node

Your Docker installation can reach its engine. Docker Compose is also available.

A single instance can look healthy until it stops. This step creates that outage on purpose so you can see its failure domain before redesigning it.

In this step, get ready to:
  • Configure one nginx web node with a plain-text identity.
  • Prove the running service returns served-by-web.
  • Stop the only node to expose the single-instance outage.
Configure the web node

nginx needs a server block that identifies this node in every response. The Compose file turns that configuration into a repeatable local service.

  • Select the web folder in VS Code's file sidebar.
  • Use the sidebar's new-file icon.
  • Type default.conf in the filename field.
  • Press Enter.
  • Define the backend response by pasting this configuration into web/default.conf:
server {
    listen 80;
    default_type text/plain;

    location / {
        return 200 "served-by-web";
    }
}

What does this configuration do?

  • The listen 80 directive accepts requests on the container's web port.
  • The default_type text/plain directive makes the response readable as plain text.
  • The return 200 "served-by-web" directive gives every successful response this node's identity.
  • Save web/default.conf.
  • Confirm default.conf appears inside web in VS Code's file sidebar.

Can't find the configuration file?

  • Expand the web folder in the file sidebar.
  • Check that the filename is exactly default.conf.

Help me find my nginx configuration file.

  • Select the resilient-web workspace folder in VS Code's file sidebar.
  • Use the sidebar's new-file icon.
  • Type compose.yml in the filename field.
  • Press Enter.
  • Define the single web service by pasting this configuration into compose.yml:
services:
  web:
    image: nginx:1.30.5-alpine
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./web/default.conf:/etc/nginx/conf.d/default.conf:ro

How does the service fit together?

  • The web service uses the pinned nginx:1.30.5-alpine image.
  • The port mapping sends requests from 127.0.0.1:8080 to nginx on container port 80.
  • The volume mounts web/default.conf into nginx's configuration directory.
  • The ro access mode keeps the mounted configuration read-only inside the container.
  • Save compose.yml.
  • Switch back to the terminal you used while creating the resilient-web workspace.

Before you validate the file, do you expect Docker Compose to render one service or several?

  • Validate the Compose model by running this command:
docker compose config

What does this command check?

Docker Compose parses the file before rendering its effective model. This catches structural YAML problems before any container starts.

You should see a rendered services model containing only web. Its output should show the pinned image plus the port mapping and read-only configuration mount.

Configuration validation failed?

  • Check the indentation in compose.yml.
  • Confirm ports aligns with image under web.
  • Confirm volumes aligns with ports.

Help me fix my Compose configuration.

✔️ Awesome, I've got everything!

Both configuration files now match the single-node design.

ⓧ I'd like to double check the full code

  • Compare compose.yml with this complete version:
services:
  web:
    image: nginx:1.30.5-alpine
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./web/default.conf:/etc/nginx/conf.d/default.conf:ro

This file defines the complete single-service Compose model for this step.

  • Compare web/default.conf with this complete version:
server {
    listen 80;
    default_type text/plain;

    location / {
        return 200 "served-by-web";
    }
}

This file defines the complete plain-text response for the first web node.

Launch and probe the service

The rendered model is valid. Detached startup keeps the service running after Docker Compose returns control of the terminal.

  • Create the web container by running this command:
docker compose up --detach

What happens during startup?

  • Docker Compose creates the web container.
  • nginx starts in the background with your mounted configuration.
  • Docker publishes the service only on the local machine at 127.0.0.1:8080.

If Docker needs the pinned image, the terminal prints download progress before the service starts.

  • Confirm the terminal reports that the web service started.

Web service did not start?

  • Confirm the earlier configuration check completed successfully.
  • Check that web/default.conf is saved.
  • Check that Docker Engine remains reachable.

Help me diagnose the web container startup.

A one-off BusyBox container can send the HTTP request without relying on browser tooling. Host networking lets that probe reach the localhost port published by Docker.

Before you send the request, what backend identity do you expect to receive?

  • Send a request to the web service by running this command:
docker run --rm --network host busybox:1.37.0 wget -qO- http://127.0.0.1:8080

How does the probe work?

  • The --rm flag removes the probe container when the command exits.
  • The --network host option shares the Linux host's network namespace.
  • The wget -qO- portion writes the response body to the terminal.

You'll see served-by-web in the terminal. That first response proves your containerized endpoint is live.

No served-by-web response?

  • Confirm the web service startup completed.
  • Check that the port mapping in compose.yml contains 127.0.0.1:8080:80.
  • Check that web/default.conf returns served-by-web.

Help me troubleshoot the BusyBox probe.

Stop the node and expose the outage

A resilience claim only matters after a controlled failure test. You will now remove the only serving instance from the request path.

  • Stop the web service by running this command:
docker compose stop web

What does stopping the service change?

Docker Compose stops the web container without removing it. Its published localhost port no longer has a running nginx process behind it.

Before you repeat the probe, do you think any service remains available at 127.0.0.1:8080?

  • Repeat the same request by running this command:
docker run --rm --network host busybox:1.37.0 wget -qO- http://127.0.0.1:8080

This failure is intentional

You do not receive served-by-web because the only backend is stopped. The probe exits unsuccessfully because nothing is listening on the published endpoint.

You have now proved that the single node is also the whole service. One stopped instance causes a complete outage.

Still receiving served-by-web?

  • Check the terminal output from the stop command for the web service.
  • Repeat the stop command shown above.
  • Run the probe again after the stop completes.

Help me verify the controlled outage.

Your first web node worked until its only instance stopped. Next, you'll add a stable proxy endpoint plus a second backend so one failure no longer removes the whole service.

Add a Proxy and Second Node

Your controlled outage proved that one nginx container made the whole platform fragile. The service vanished as soon as that instance stopped.

Now you'll place a stable reverse proxy in front of two independently replaceable web nodes. Docker Compose will run the three services from one model.

In this step, get ready to:
  • Create a second web node with its own response identity.
  • Configure an nginx proxy for both backends.
  • Verify round-robin distribution across the running services.
Create the second node and proxy configuration

Each backend needs a distinct response identity. Repeated requests can then reveal which node handled each one.

  • Create web2/default.conf inside the web2 folder from earlier by using the VS Code Explorer sidebar.
  • Define the second node's response by pasting this configuration into web2/default.conf:
server {
    listen 80;
    default_type text/plain;

    location / {
        return 200 "served-by-web2";
    }
}

What Does This Configuration Do?

  • The server listens for HTTP traffic on container port 80.
  • The response uses the text/plain content type.
  • Every request returns served-by-web2 as the node identity.
  • Save web2/default.conf in VS Code.
  • Confirm default.conf appears inside the web2 folder in the Explorer sidebar.

Is the File in the Wrong Folder?

Check the file's indentation in the Explorer sidebar. Recreate it inside web2 if it appears beside that folder.

Use this if the path still looks wrong: Help me place web2/default.conf in the correct folder.

The proxy can reach both backends through Compose service discovery. Service names provide stable destinations even when container addresses change.

  • Create proxy/default.conf inside the proxy folder from earlier by using the VS Code Explorer sidebar.
  • Define the backend pool by pasting this block into proxy/default.conf:
upstream app_pool {
    server web:80 max_fails=1 fail_timeout=5s;
    server web2:80 max_fails=1 fail_timeout=5s;
}

How Does the Backend Pool Work?

  • The app_pool group gives the proxy one routing target for both backends.
  • The names web and web2 match the Compose service names.
  • The max_fails=1 parameter records a backend failure after one failed attempt.
  • The fail_timeout=5s parameter sets the temporary failure window.
  • Save proxy/default.conf in VS Code.
  • Confirm default.conf appears inside the proxy folder in the Explorer sidebar.

Do the Backend Names Look Different?

Use web and web2 exactly. These names must match the services in compose.yml.

Use this to check the pool: Help me check my nginx upstream configuration.

  • Complete the proxy endpoint by adding this server block below app_pool in proxy/default.conf:
server {
    listen 80;

    location / {
        proxy_pass http://app_pool;
        proxy_next_upstream error timeout;
        add_header Cache-Control "no-store" always;
    }
}

How Does the Proxy Route Requests?

  • The proxy_pass directive sends incoming requests to app_pool.
  • The proxy_next_upstream error timeout rule permits another backend to handle a connection error or timeout.
  • The Cache-Control header prevents stored responses from hiding which backend handled a request.
  • Save proxy/default.conf in VS Code.
  • Confirm the server block sits below the closing brace of app_pool.

Is the Server Block Nested?

Check that the upstream block closes before the server block begins. A missing brace can place the server inside app_pool.

Use this to inspect the structure: Help me fix the structure of proxy/default.conf.

Move the localhost port to the proxy

The existing Compose model publishes the first backend directly. The redesigned model gives that localhost entry point to the proxy.

  • Select the existing compose.yml file in the VS Code Explorer sidebar.
  • Replace the entire file by pasting this three-service model:
services:
  proxy:
    image: nginx:1.30.5-alpine
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./proxy/default.conf:/etc/nginx/conf.d/default.conf:ro

  web:
    image: nginx:1.30.5-alpine
    volumes:
      - ./web/default.conf:/etc/nginx/conf.d/default.conf:ro

  web2:
    image: nginx:1.30.5-alpine
    volumes:
      - ./web2/default.conf:/etc/nginx/conf.d/default.conf:ro

What Changed in the Compose Model?

  • The proxy service now owns the localhost port mapping.
  • The web service keeps its existing read-only configuration mount.
  • The web2 service mounts its new response configuration.
  • Neither backend publishes a host port.
  • Save compose.yml in VS Code.
  • Confirm only the proxy service contains a ports section.

Is the Port Still on the Backend?

Remove the original ports section from web. Keep the localhost mapping beneath proxy.

Use this to check the service structure: Help me inspect my three-service Compose model.

✔️ Awesome, I've got everything!

Your four configuration files now describe one proxy with two separate backends. Double-check that every file is saved before you start the stack.

ⓧ I'd like to double check the full code

services:
  proxy:
    image: nginx:1.30.5-alpine
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./proxy/default.conf:/etc/nginx/conf.d/default.conf:ro

  web:
    image: nginx:1.30.5-alpine
    volumes:
      - ./web/default.conf:/etc/nginx/conf.d/default.conf:ro

  web2:
    image: nginx:1.30.5-alpine
    volumes:
      - ./web2/default.conf:/etc/nginx/conf.d/default.conf:ro

What Should This File Contain?

This file defines all three services. Only proxy publishes the localhost port.

upstream app_pool {
    server web:80 max_fails=1 fail_timeout=5s;
    server web2:80 max_fails=1 fail_timeout=5s;
}

server {
    listen 80;

    location / {
        proxy_pass http://app_pool;
        proxy_next_upstream error timeout;
        add_header Cache-Control "no-store" always;
    }
}

What Should This File Contain?

This file combines the two-backend pool with the proxy endpoint. The endpoint forwards requests through app_pool.

server {
    listen 80;
    default_type text/plain;

    location / {
        return 200 "served-by-web";
    }
}

What Should This File Contain?

This existing backend keeps the response identity served-by-web.

server {
    listen 80;
    default_type text/plain;

    location / {
        return 200 "served-by-web2";
    }
}

What Should This File Contain?

This second backend uses the distinct response identity served-by-web2.

Start and verify the three-service platform

Compose can render the combined model before it changes the containers. This catches structural errors in the service definitions or bind mounts.

  • Validate the three-service model by running this command in the terminal from earlier:
docker compose config

What Does This Command Confirm?

The command parses the Compose file. It renders the model that Docker Engine uses to create the services.

You should see rendered sections for proxy web web2. Only proxy should show the localhost port mapping.

Did Configuration Validation Fail?

Check the indentation beneath each service name in compose.yml. Check that each bind mount points to one of the saved configuration files.

Use this to diagnose the model: Help me fix my Docker Compose configuration.

  • Create or update the three running services by running:
docker compose up --detach

What Does Detached Startup Do?

Compose creates the proxy and second backend. It also returns the stopped first backend to the running stack.

You should see terminal output showing the three services being created or started. The command returns control to your terminal while the containers continue running.

Did a Service Fail to Start?

Check that every configuration file is saved. Check that the bind mount paths in compose.yml match the folders shown in the Explorer sidebar.

Use this to inspect the startup failure: Help me diagnose why my three-service Compose stack did not start.

A four-request BusyBox probe makes the traffic distribution visible. Before you run it, which backend identities do you expect to see across the responses?

  • Send four requests through the localhost proxy by running:
docker run --rm --network host busybox:1.37.0 sh -c 'for i in 1 2 3 4; do wget -qO- http://127.0.0.1:8080; echo; done'

What Does the Probe Prove?

The loop sends four separate requests to the proxy. nginx uses round-robin load balancing to distribute those requests across the two available backends.

You should see both served-by-web and served-by-web2 in the output. Both identities prove that one localhost endpoint reaches two separate nodes.

Do You See Only One Identity?

Check that app_pool contains both backend server entries. Check that the two response files return different identities.

Use this to trace the request path: Help me find why my proxy only returns one backend identity.

That is the architecture shift working. Next, you'll restrict backend exposure and add explicit health signals.

Harden the Network Boundary

Your nginx reverse proxy already distributes requests across two backends. Load balancing improves availability without defining a strict boundary around those backends.

The current Docker Compose model relies on an implicit shared network. It also starts the proxy without waiting for explicit health signals.

In this step, get ready to:
  • Separate host-facing traffic from the internal backend network.
  • Add health checks plus health-based startup dependencies.
  • Validate the model before confirming that every service is healthy.
Segment the service networks

Network segmentation gives each traffic path a clear purpose. The edge network carries proxy traffic while the internal backend network connects the proxy to both web nodes.

  • Return to compose.yml in VS Code.
  • Scroll below the web2 service definition.
  • Add the network definitions using this configuration:
networks:
  edge:
    driver: bridge
  backend:
    driver: bridge
    internal: true

What do these networks control?

  • The edge bridge network gives the proxy a dedicated traffic boundary.
  • The backend bridge network carries traffic between the proxy and web nodes.
  • The internal: true setting creates an externally isolated network.
  • Save compose.yml.
  • Render the updated model by running this command in the resilient-web terminal:
docker compose config

What does this check prove?

The command parses the Compose file before rendering its effective model. A successful result confirms that both network definitions use valid Compose syntax.

You'll see rendered definitions for edge and backend. The backend definition includes internal: true.

Configuration check failed?

Check that networks: starts at the far left. Make sure edge and backend use two spaces of indentation.

Help me fix the network definitions in my Compose file.

  • Find the volumes section inside the proxy service.
  • Add this network membership below the proxy volume mount:
    networks:
      - edge
      - backend

Why does the proxy join both?

The proxy receives traffic through edge. It reaches the two web services through backend.

  • Save compose.yml.
  • Confirm the proxy network memberships by rerunning:
docker compose config

What changed in the model?

The rendered proxy service now lists both networks. This makes the proxy the bridge between incoming traffic and the internal services.

You'll see edge plus backend under the rendered proxy service.

  • Find the volumes section inside the web service.
  • Add this network membership below the web volume mount:
    networks:
      - backend

What does this membership enforce?

The web service joins only the internal backend network. Its nginx process remains reachable from the proxy by service name.

  • Save compose.yml.
  • Check the updated web service by rerunning:
docker compose config

What should the rendered service show?

The rendered web service lists only backend. It has no published host port.

You'll see the first web node attached exclusively to the backend network.

  • Find the volumes section inside the web2 service.
  • Add the same backend membership below the web2 volume mount:
    networks:
      - backend

How is the boundary completed?

The second web node now shares the same isolated backend network. Both nodes remain discoverable through their Compose service names.

  • Save compose.yml.
  • Verify all three network memberships by rerunning:
docker compose config

What should the boundary look like?

  • The proxy service lists edge plus backend.
  • The web service lists only backend.
  • The web2 service lists only backend.

The rendered model now shows one host-facing proxy path plus two internal-only backend services.

Service attached to the wrong network?

Check that each networks block aligns with image plus volumes inside its service. Make sure only the proxy lists edge.

Help me check my Compose network memberships.

Gate startup with health checks

A health check gives Compose an explicit readiness signal. Here, nginx -t checks the nginx configuration syntax plus its referenced files.

What does this health check prove?

The check proves that nginx can parse its configuration. It also proves that nginx can open the referenced files.

Request-path failover still comes from the proxy's passive upstream checks. You prove that behavior during the controlled failure test in the next step.

  • Find the volumes section inside the proxy service.
  • Add the dependency plus health configuration below the proxy volume mount:
    depends_on:
      web:
        condition: service_healthy
      web2:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "nginx", "-t"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 30s

How does the proxy startup gate work?

  • The depends_on conditions make the proxy wait for both web services to become healthy.
  • The test runs nginx -t inside the proxy container.
  • The timing fields control the check interval plus timeout. They also define the retry allowance plus startup grace period.
  • Save compose.yml.
  • Render the proxy health gate by running:
docker compose config

What should the proxy model contain?

The rendered proxy includes both service_healthy conditions. It also includes the complete nginx -t health check.

You'll see web plus web2 listed under the proxy dependency section.

Proxy dependency missing from the model?

Place depends_on at the same indentation level as volumes. Place each condition beneath its matching service name.

Help me fix the proxy health dependency configuration.

  • Find the volumes section inside the web service.
  • Add this health check below the web volume mount:
    healthcheck:
      test: ["CMD", "nginx", "-t"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 30s

What signal does web provide?

The web service reports healthy after nginx accepts its configuration. Compose can use that signal before creating the proxy.

  • Save compose.yml.
  • Confirm the first backend health check by running:
docker compose config

What should web show?

The rendered web service now contains the complete health check. Its network remains backend.

You'll see the same timing values that you assigned to the proxy health check.

  • Find the volumes section inside the web2 service.
  • Add the matching health check below the web2 volume mount:
    healthcheck:
      test: ["CMD", "nginx", "-t"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 30s

Why must both backends report health?

The proxy depends on a health signal from each backend. Matching checks give both nodes the same startup contract.

  • Save compose.yml.
  • Confirm the complete health model by running:
docker compose config

What should every service contain?

The rendered model includes an nginx -t health check for proxy, web, plus web2.

All three checks use 10s intervals plus timeouts. They use 5 retries plus a 30s start period.

Backend health check missing?

Check that each healthcheck block belongs inside its service. Keep it aligned with image, volumes, plus networks.

Help me compare the three Compose health checks.

✔️ Awesome, I've got everything!

Your compose.yml now defines the complete network boundary plus health-based startup model.

ⓧ I'd like to double check the full code

Compare your saved compose.yml with this complete version.

services:
  proxy:
    image: nginx:1.30.5-alpine
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./proxy/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      web:
        condition: service_healthy
      web2:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "nginx", "-t"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 30s
    networks:
      - edge
      - backend

  web:
    image: nginx:1.30.5-alpine
    volumes:
      - ./web/default.conf:/etc/nginx/conf.d/default.conf:ro
    healthcheck:
      test: ["CMD", "nginx", "-t"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 30s
    networks:
      - backend

  web2:
    image: nginx:1.30.5-alpine
    volumes:
      - ./web2/default.conf:/etc/nginx/conf.d/default.conf:ro
    healthcheck:
      test: ["CMD", "nginx", "-t"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 30s
    networks:
      - backend

networks:
  edge:
    driver: bridge
  backend:
    driver: bridge
    internal: true

What should you compare?

Check the indentation of each service-level block. Confirm that the top-level networks section aligns with services.

Validate the hardened stack

The complete model now has explicit traffic boundaries plus readiness gates. The final check proves that Compose accepts the model before starting every service in the intended state.

Before you validate the file, do you expect Compose to accept every network membership plus health dependency?

  • Validate the complete model by running:
docker compose config

What does the final validation prove?

Compose parses plus resolves the complete model. The rendered output shows the internal backend network plus every service attachment.

You'll see the proxy depend on both healthy backends. You'll also see internal: true under the backend network.

Final validation still failing?

Use the full-code tab to compare each indentation level. YAML nesting determines whether a setting belongs to one service or the complete project.

Help me diagnose my final Compose validation failure.

A short pause during startup is normal because Compose waits for each health check to pass.

  • Start the hardened stack by running:
docker compose up --detach --wait

What does this startup command do?

Compose updates the stack in detached mode. The wait option keeps the command active until the services reach their running or healthy state.

The command returns control to your terminal after the health conditions pass.

Stack did not become healthy?

Check the service named in the terminal output. Compare its volume path with the matching configuration file inside the resilient-web workspace.

Help me diagnose a service that did not become healthy.

Before you inspect the stack, which service do you expect to show a published host port?

  • Inspect service health plus published ports by running:
docker compose ps

Read the hardened status

  • The proxy service shows a healthy running state.
  • The web service shows a healthy running state.
  • The web2 service shows a healthy running state.
  • Only the proxy shows 127.0.0.1:8080->80/tcp under its published ports.

That's the boundary secured. All three services are healthy while the two backend nodes remain hidden behind the proxy.

Status or ports look different?

If a service lacks a healthy status, check its healthcheck indentation. If a backend shows a published port, remove any ports block from that backend service.

Help me interpret my Compose service status.

Your platform now has an isolated backend network plus health-gated startup. Next, you'll stop one backend and trace how the resilient design handles the incident.

Prove Failover and Trace the Incident

Your Docker Compose stack now has two healthy backends behind one nginx proxy. The proxy gives each request another route when a backend disappears.

The original stop caused an outage because the platform had no surviving node. This step repeats that controlled failure against the redundant design.

Container events establish the lifecycle timeline. Proxy logs show how request handling reacts.

In this step, get ready to:
  • Capture the backend lifecycle during a controlled stop.
  • Prove continued traffic through the surviving backend with proxy log evidence.
  • Restore the stopped backend to healthy service.
Watch the backend stop

A container event stream records lifecycle activity as it happens. Starting the stream before the failure gives you a timeline of the controlled incident.

  • Switch back to the VS Code terminal for the resilient-web workspace.
  • Stream Docker Engine events by running this command:
docker events

What does the event stream show?

The stream subscribes to Docker Engine lifecycle activity. Each line builds a timeline for later correlation.

Stopping one backend is safe in this local test. The proxy plus web2 remain active.

  • Create a second terminal tab from VS Code's terminal panel.
  • Stop the web service by running this command:
docker compose stop web

What happened to the container?

Docker Compose stops the existing web container without removing it. This preserves the container so you can start it again during recovery.

  • Return to the first terminal showing the event stream.

You should see a kill action followed by die. A stop action completes the lifecycle.

  • End the event stream by pressing Ctrl+C in the first terminal.

Missing the lifecycle events?

Confirm the event stream was still running when you stopped web. Focus on entries associated with the stopped web service container.

If the stream ended too early, start it again before repeating the controlled stop.

Help me trace the missing Docker events.

Prove traffic survives

The reverse proxy uses passive failure detection on request traffic. A failed attempt to reach web:80 can move to web2:80 through proxy_next_upstream error timeout.

Before you run the probe, picture whether any responses should fail while web is stopped.

  • Send four host-network requests from BusyBox by running this command:
docker run --rm --network host busybox:1.37.0 sh -c 'for i in 1 2 3 4; do wget -qO- http://127.0.0.1:8080; echo; done'

What does this prove?

All four responses should print served-by-web2. The proxy has continued through the surviving backend.

That is the key resilience proof. One backend is unavailable while the platform still serves successful responses.

Did the requests fail?

Confirm web2 plus proxy are still running. Make sure the probe targets http://127.0.0.1:8080.

Help me diagnose the failed failover probe.

Successful client responses reveal the surviving path. Proxy logs expose the failed upstream attempt behind that success.

  • Inspect the proxy evidence by running this command:
docker compose logs proxy

What should you look for?

Look for proxy entries from the failure window that point to the unavailable web upstream. This evidence explains how a successful client response can coexist with an upstream failure.

No upstream failure evidence?

Run the four-request probe again while web remains stopped. Repeat the proxy log check immediately afterward.

Help me correlate the proxy logs with the failed upstream.

Restore the healthy stack

Recovery should reuse the stopped container. Its existing configuration plus network membership remain available.

  • Start the existing web container by running this command:
docker compose start web

What does this command restore?

Docker Compose starts the stopped service container. The nginx health check resumes testing its configuration with nginx -t.

Recovery can take one health-check cycle. That short pause means Docker is evaluating the backend before reporting it as healthy.

Before you check, picture the health state that web must reach before it can serve traffic again.

  • Inspect the recovered stack by running this command:
docker compose ps

What should recovery look like?

The proxy service should be running with a healthy status. Both backend services should show the same state. Only the proxy should show 127.0.0.1:8080->80/tcp.

Is web still starting?

Allow another health-check cycle before repeating the status check. If web remains unhealthy, compare its mounted configuration with the validated web/default.conf file.

Help me restore the unhealthy backend.

Before the final probe, picture how the output should differ from the earlier single-backend result.

  • Confirm both backends serve requests again by running this command:
docker run --rm --network host busybox:1.37.0 sh -c 'for i in 1 2 3 4; do wget -qO- http://127.0.0.1:8080; echo; done'

What does the final probe prove?

You should see both served-by-web plus served-by-web2 in the output. Round-robin distribution has resumed after recovery.

You have now proved the full incident cycle. The platform survived a backend stop before returning to normal distribution.

Only seeing one backend?

Give nginx's 5s failure window time to expire before repeating the probe. Confirm docker compose ps reports both backends as healthy.

Help me restore round-robin responses.

Your controlled incident now has a complete evidence trail from stop to failover to recovery.

Secret mission

Add Weighted Routing

Your proxy currently gives both backends an equal share of incoming requests. Change the upstream policy so the first backend receives more traffic. Then prove the bias with an eight-request sample while both nodes remain available.

Clean Up Your Resources

Clean Up Your Resources

This local Docker platform has no ongoing cloud costs. Decide whether to keep it running, pause it for later, or remove its runtime resources.

Resources you used:

  • Three local nginx containers named proxy, web, and web2.
  • Two Docker bridge networks named edge and backend.

Keep everything running

Keeping the stack active suits continued failover practice or interview demonstrations. No cleanup action is required.

  • The three containers remain available through the proxy at 127.0.0.1:8080.
  • The weighted routing policy continues sending a larger share of requests to served-by-web.
  • The four configuration files remain in the resilient-web workspace.

Pause - I'll come back to this later

A paused stack releases its active runtime resources. Your containers remain available for a later restart.

  • Switch back to the terminal from the final service check.
  • Stop all three services by running this command:
docker compose stop

What does this command do?

Docker Compose stops the services without removing their containers. Your weighted proxy configuration stays in the resilient-web workspace.

You'll see the terminal report that proxy, web, and web2 have stopped.

  • Restart the existing containers when you return by running this command:
docker compose start

How does restarting work?

This command starts the existing stopped service containers. It preserves the network layout and weighted routing configuration.

You'll see the terminal report that all three services have started.

Delete - I don't want to use this again

Deleting the runtime looks decisive. Your four configuration files remain in the resilient-web workspace so you can rebuild the platform later.

  • Switch back to the terminal from the final service check.
  • Remove the project containers and networks by running this command:
docker compose down

What does this command remove?

Docker Compose stops the project services. It removes the proxy, web, and web2 containers.

It also removes the edge and backend networks.

You'll see removal progress for the containers and networks. Your local runtime is now clean.

Nice Work!

Nice Work!

You did it! You built a local web platform with Docker Compose and nginx. It keeps answering requests when one backend stops.

You've learned how to:

  • Expose the platform through one reverse proxy endpoint. Use network segmentation to keep both backend services away from host ports.
  • Distribute requests with round-robin load balancing. Use passive failure detection to route requests through the surviving node.
  • Prove continued availability during a controlled backend stop. Build an incident timeline from container events. Confirm the failure in proxy logs. Use health checks to verify recovery.
  • Secret Mission: Apply weighted routing so web receives a larger share of requests. Prove the policy with eight repeated requests.

Ready to quiz yourself?