Source
The source code used throughout this series can be found here: https://github.com/anthoturc/token-service/.
Recap
I covered how I set up Go web services and included:
- HTTP Server Config
- Graceful shutdown
- Managing Application Configuration
- Telemetry (using OpenTelemetry)
If you haven’t read my previous articles yet, check out part one and part two! This post will cover deploying the application to Digital Ocean.
Table Of Contents
Open Table Of Contents
Picking Your Platform
In this post, I’ll use Digital Ocean’s (DO) App Platform to deploy a Docker container. I chose it because DO’s pricing is easy to understand and their developer tools (e.g., doctl
) are helpful.
If you are deploying an application to production, you should carefully weigh the pros and cons of the platform you want to use. Here are a few good questions to ask:
- What is the platform’s reputation with respect to customer support? Do non-Enterprise customers get support?
- Does the platform have published SLAs?
- Does the platform provide SDKs and CLIs to interact with its resources?
- Is the platform’s pricing easy to understand?
- How distributed is the platform? E.g., is X service available in more than one region?
If I were writing this a few years ago, I would have used Linode. I recently looked at DO’s offerings and was excited to try it out. In theory, every major platform (AWS, GCP, Azure, etc.) can accomplish what I will show in this post.
Docker
I am opting for Docker because it is what I have the most familiarity with but it is not the sole option for containerization. You could opt for Podman, Buildah, or any other engine that builds OCI container images.
Dockerfile
Let’s get the Dockerfile
created:
FROM golang:1.22-bookworm
WORKDIR /usr/local/src
COPY . .
RUN go build -o token-service .
ENV ENVIRONMENT prod
ENTRYPOINT [ "/usr/local/src/token-service" ]
This Dockerfile is pretty simple. It does the following:
- Starts off with the Debian 12 base image
- Creates
/usr/local/src
directory - Copies the contents of the working directory (the source root) into the
/usr/src/local/
directory - Builds a binary named token-service
- Sets an the
ENVIRONMENT
environment variable toprod
- Specifies the entrypoint by the path to the executable
Now let’s build the image and run it.
docker build -t token-service .
docker run --network host --rm --name token-service token-service
In a separate shell, I am retrieving a token:
$ curl -X POST localhost:8080/api/token
{"data":"o1jLDHgY+O62ZYc0f9PAOk3Q+sa2OeEAVNQn9WAuhPioMmDsBDh7eUePwaMaJ+3nuU5cBArMmpn7Ol64dteVjQ==","expires_at":"2024-03-19T15:46:48.955591709Z"}
This looks really good so far.
At this point you might be wondering: “What is this token-service?”. Details regarding ‘WHAT’ we are building have intentionally been omitted because the “HOW” is much more important. I have been building this token-service — really a toy example — to demonstrate these concepts on my YouTube channel.
Smaller Images
Anytime you are working with Docker, I recommend focusing on achieving three key objectives: 1) ensuring the image works, 2) structuring it optimally, and 3) minimizing its size.
The first point is already covered as the image was built and run successfully. Since this image is relatively basic, there might not be much value in trying to optimize the structure of the Dockerfile. However, an optimally structured image will take advantage of caching build steps as much as possible. Typically, this involves placing the steps that don’t change towards the top of the file.
Let’s focus on the size of the image by inspecting its current size.
$ docker images token-service
REPOSITORY TAG IMAGE ID CREATED SIZE
token-service latest ed7c33c85d34 22 minutes ago 1.09GB
Woah! 1GB seems steep for running a simple service. However, the generated image includes all the utilities and content that come with the Debian 12 base image. Most of that isn’t actually necessary or useful for running a token service. Additionally, your application should not depend on a specific distribution/OS.
Enter, distroless. Folks at Google (and I am sure others) realized you shouldn’t need a bloated image to run your application. A distroless image combined with a multistage build will give us a smaller image and better security posture since we won’t have to worry about CVEs associted with extra software in the Debian 12 base image.
Let’s update our Dockerfile, build the image, and run the service as a sanity check.
FROM golang:1.22-bookworm as builder
WORKDIR /usr/local/src
COPY . .
RUN go build -o token-service .
RUN chmod +x token-service
FROM gcr.io/distroless/base-nossl-debian12
WORKDIR /usr/local/src
COPY --from=builder /usr/local/src/configuration ./configuration
COPY --from=builder /usr/local/src/token-service .
ENV ENVIRONMENT prod
ENTRYPOINT [ "/usr/local/src/token-service" ]
$ docker build -t token-service .
$ docker images token-service
REPOSITORY TAG IMAGE ID CREATED SIZE
token-service latest 2c551a757d95 5 seconds ago 32.9MB
That is a 30X improvement in image size! Before we celebrate, spin up a container to make sure the application still works as expected. Usually you would do this with integration testing and canaries but we are prototyping for now.
docker run --rm --name token-service --network host token-service
$ curl -X POST localhost:8080/api/token
{"data":"1NVMrKZyGU1OcZetXlFJyh5X/x72lUm146VvqKgLTM/fL7kyf19P3atVoAfIKpi+OmU2LL8wRQ8ka07oa9iQ1w==","expires_at":"2024-03-19T16:32:59.369649867Z"}
Awesome! Things are working as expected. I tried optimizing the Docker image pretty quickly, but I would generally advise against optimizing too soon. In this case, the image size was easy to “fix” because I wanted to stay within the free tier on DO’s container registry. However, there are tradeoffs to consider. For example, with distroless images, you don’t have a shell, which may impact troubleshooting and debugging.
Let’s get to deployments!
DO App Platform
We have a Dockerfile, now we want to get it deployed so the application can be accessed! I mentioned this earlier in the Picking Your Platform section, but I will be using DO to deploy this application.
If you are following along, you will need to:
- *Sign up for a DO Account (I signed up with GitHub)
- Create an API token
- Install the doctl CLI
- **Install the DO GitHub app in your personal repository
*You need a Credit Card for sign up but, if tear down your application quickly, you shouldn’t be charged anything for deploying this application.
**I am using GitHub but this should work for other providers (Bitbucket, GitLab, etc.).
If everything was setup properly, you should be able to authenticate with DO in your terminal. You shoud use the API token you generated previously.
doctl auth init -t dop_v1_a668...
Using token for context default
Validating token... ✔
App Spec
Let’s get the App Spec set up so we can actually deploy something. Here is my spec.yml
:
name: token-service
region: nyc
services:
- name: token-service
dockerfile_path: Dockerfile
source_dir: .
github:
branch: mainline
deploy_on_push: true
repo: anthoturc/token-service
health_check:
http_path: /api/healthz
port: 8080
http_port: 8080
instance_count: 1
instance_size_slug: basic-xxs
# All paths are handled by this application
routes:
- path: /
Check out the DO docs for more information on the app sec.
You will need to specify a /health
endpoint so that DO can check that the app is healthy once the container is started.
Deploy
If you have followed, along so far, deploying is super easy!
doctl apps create --spec spec.yml
Notice: App created
ID Spec Name
45b70dca-c55c-42e3-a019-3251ded78c1b token-service
I omitted some of the output, but the important details are included. By the way, this is the only time we have to do this. Going forward, the application should be updated automatically anytime we push to the mainline of the repository. That is part of the CI/CD journey. In this case, we have enabled the CD portion. The CI portion would come from setting up GitHub Actions.
DO should provide a full endpoint you can use for the application. In my case it is: token-service-k6pht.ondigitalocean.app
I can ping the API here:
curl -X POST https://token-service-k6pht.ondigitalocean.app/api/token
{"data":"FnLdlK5GYxPDKG1JOhgMve0qOYudetdQ2tszmiwSXduaLybseegKOQJx+Port1sL9ThfEEaU2nJX29PV653Www==","expires_at":"2024-03-19T20:03:15.073911888Z"}
Conclusion
In this post, we covered:
- Docker-izing a Go application
- Deploying to DO’s App Platform
That wraps up the last part of this short series! I might write a future post on setting up CI/CD via GitLab or GitHub and configuring the application to expose Go-specific metrics.
Once again, the source code to the repo used throughout this series is located here: https://github.com/anthoturc/token-service/