DEV Community

Cover image for Day 45: RUN Cannot See Your Build Context, and NAT Only Goes One Way
Nnamdi Felix Ibe
Nnamdi Felix Ibe

Posted on AI-assisted

Day 45: RUN Cannot See Your Build Context, and NAT Only Goes One Way

Both of today's tasks are about direction. A Dockerfile instruction that cannot reach backwards to the machine you are building on. A gateway that lets traffic out and never lets it in.

One Docker task, one AWS task. Fix a Dockerfile that will not build, then give a private-subnet instance outbound internet access. The tasks come from the KodeKloud Engineer platform.

The error is about location, not spelling

cd /opt/docker/
docker build -t nautilus:latest_img .
Enter fullscreen mode Exit fullscreen mode
cp: cannot stat 'certs/server.crt': No such file or directory
Enter fullscreen mode Exit fullscreen mode

The file is right there. ls shows certs/server.crt sitting next to the Dockerfile. The line that failed:

RUN cp certs/server.crt /usr/local/apache2/conf/server.crt
Enter fullscreen mode Exit fullscreen mode

cp is not wrong and the path is not wrong. The instruction is. RUN executes a command inside the image filesystem, where certs/ has never existed. The file is in the build context, on the host, and RUN cannot see it.

COPY certs/server.crt /usr/local/apache2/conf/server.crt
COPY certs/server.key /usr/local/apache2/conf/server.key
COPY html/index.html /usr/local/apache2/htdocs/
Enter fullscreen mode Exit fullscreen mode

COPY takes files from the build context and puts them into the image. That is the one instruction that spans the boundary, and it is the only way anything on your machine gets into the build.

What makes this a good puzzle is that the same file also has four RUN sed lines that are completely fine:

RUN sed -i "s/Listen 80/Listen 8080/g" /usr/local/apache2/conf/httpd.conf
Enter fullscreen mode Exit fullscreen mode

Those edit httpd.conf, which is already in the image because the base image put it there. Editing a file that exists inside the image is exactly what RUN is for. Reaching for a file on the host is not. Four correct RUN lines and three wrong ones, in the same file, differing only by which side of the boundary the file lives on.

One detail to check before blaming the Dockerfile. The trailing . on docker build is the build context, and COPY paths resolve against it. Build from the wrong directory and COPY certs/server.crt fails identically, for the opposite reason: the instruction is right and the context does not contain the file.

The NAT gateway goes in the subnet it is not serving

This one reads backwards until it clicks. A NAT gateway serving a private subnet is placed in the public subnet, and AWS states it as a requirement: you create a public NAT gateway in a public subnet and must associate an Elastic IP with it at creation.

The reason is that a NAT gateway is itself a client of the internet. It takes traffic from private instances, rewrites the source address to its own Elastic IP, and forwards it out. To forward anything it needs its own route to an internet gateway, and that route lives in the public subnet's table.

Put it in the private subnet and it creates without complaint, reaches available, and drops every packet it forwards. Nothing in describe-nat-gateways suggests a problem.

The finished path is two tables and two hops:

EC2 (private subnet)
  -> priv-rt: 0.0.0.0/0 -> nat-gateway
    -> NAT gateway (public subnet, holds the Elastic IP)
      -> pub-rt: 0.0.0.0/0 -> internet gateway
        -> internet
Enter fullscreen mode Exit fullscreen mode

The private subnet never references the IGW directly.

One way only

"Internet access" is doing a lot of work in the task description, so it is worth being precise. AWS says instances behind a NAT gateway cannot receive unsolicited inbound connections from the internet.

Internet gateway NAT gateway
Outbound from instance Yes Yes
Inbound to instance Yes No
Instance needs a public IP Yes No
Costs money at rest No Yes

The instance still has no public IP and never will. It can install packages, call AWS APIs and upload to S3, and nothing on the internet can open a connection to it. That asymmetry is the entire reason to use a NAT gateway rather than just moving the instance to a public subnet.

The Elastic IP belongs to the gateway, not the instance, so every private instance behind it shares one source address. Which is also what makes it useful when a third party wants an IP to allowlist.

Two things the task was right to insist on

It said explicitly not to touch the VPC's Main route table, and that is not fussiness. The Main table is the fallback for any subnet with no explicit association, so a 0.0.0.0/0 route there grants internet access to every unassociated subnet in the VPC, including ones created months later by someone who assumed a new subnet would be isolated. A dedicated table with an explicit association makes the intent readable and keeps everything else isolated by default.

And AZ placement is not cosmetic. AWS documents each NAT gateway as created in a specific availability zone and made redundant within that zone. Which means if resources across several AZs share one gateway and its zone goes down, the resources in the healthy zones lose internet access too. The recommendation is one per AZ with routing to match. Here one private subnet in one AZ meant one gateway in the same AZ, which is both correct and cheapest.

While we are on cost: a NAT gateway bills hourly whether anything flows through it or not, plus a per-gigabyte processing charge. For this workload, an instance in a private subnet talking only to S3, a Gateway VPC Endpoint would have done the job for nothing:

aws ec2 create-vpc-endpoint --vpc-id $VPC --service-name com.amazonaws.us-east-1.s3 \
  --route-table-ids $PRIV_RT
Enter fullscreen mode Exit fullscreen mode

Free, keeps the traffic on the AWS network, and covers S3 and DynamoDB only. The task asked for NAT, so NAT is what got built, but the cheaper answer exists.

Checks that come before the wait

Two habits carried over from Day 40, both of which turn an ambiguous outcome into a clear one.

Check the route state before waiting on the result. active rather than blackhole proves the target resolves, so an empty bucket three minutes later means the cron job has not fired yet rather than that the path is broken.

And establish the negative first. The bucket was confirmed empty before anything was built, which is what turns the later listing into evidence. A leftover file from a previous attempt would have looked exactly like success.

Which side of the line

RUN cannot reach the host. A NAT gateway cannot be reached from the internet. In both cases the thing that feels like a limitation is the boundary doing its job, and in both cases the failure mode is a command that looks reasonable and fails somewhere you were not looking.

So here is the Day 45 question. In the last thing you built, do you know which direction each connection can be opened from, or only that it works?

Day 45 down. Fifty-five to go.

Top comments (0)