Build a rock for a Go app

In this tutorial, we’ll containerise a simple Go app into a rock using Rockcraft’s go-framework extension.

It should take 25 minutes for you to complete.

You won’t need to come prepared with intricate knowledge of software packaging, but familiarity with Linux paradigms, terminal operations, and Go is required.

Once you complete this tutorial, you’ll have a working rock for a Go app. You’ll gain familiarity with Rockcraft and the go-framework extension, and have the experience to create rocks for Go apps.

Setup

We recommend starting from a clean Ubuntu installation. If we don’t have one available, we can create one using Multipass:

Is Multipass already installed and active? Check by running

snapservicesmultipass

If we see the multipass service but it isn’t "active", then we’ll need to run sudo snap start multipass. On the other hand, if we get an error saying snap "multipass" not found, then we must install Multipass:

sudosnapinstallmultipass

Then we can create the VM with the following command:

multipass launch --memory 2G --disk 10G --name rock-dev 26.04

Finally, once the VM is up, open a shell into it:

multipassshellrock-dev

Unless stated otherwise, we will work entirely within the VM from now on.


LXD will be required for building the rock. Make sure it is installed and initialised:

sudosnapinstalllxd
lxdinit--auto

There is a known connectivity issue with LXD and Docker. To avoid this issue, enable IPv4 forwarding before installing Docker:

echo"net.ipv4.conf.all.forwarding=1"|sudotee-a/etc/sysctl.d/99-forwarding.conf
sudosystemctlrestartsystemd-sysctl

Warning

In a production environment, make sure to use a hardened firewall configuration.

In order to create the rock, we’ll install Rockcraft with classic confinement, which grants it access to the whole file system:

sudosnapinstallrockcraft--classic

We’ll use Docker to run the rock. We can install it as a snap:

sudosnapinstalldocker

By default, Docker is only accessible with root privileges (sudo). We want to be able to use Docker commands as a regular user:

sudoaddgroup--systemdocker
sudoadduser$USERdocker
newgrpdocker

Restart Docker:

sudosnapdisabledocker
sudosnapenabledocker

Note that we’ll also need a text editor. We can either install one of our choice or simply use one of the already existing editors in the Ubuntu environment (like nano).

In order to test the Go app locally, before packing it into a rock, install Go.

sudosnapinstallgo--classic

Create the Go app

Start by creating the "Hello, world" Go app that will be used for this tutorial.

Create an empty project directory:

mkdirgo-hello-world
cdgo-hello-world

Initialise the new Go module:

gomodinitgo-hello-world

Create a main.go file, copy the following text into it and then save it:

~/go-hello-world/main.go
packagemain
import(
"fmt"
"log"
"net/http"
)
funchelloWorldHandler(whttp.ResponseWriter,req*http.Request){
log.Printf("new hello world request")
fmt.Fprintln(w,"Hello, world!")
}
funcmain(){
log.Printf("starting hello world application")
http.HandleFunc("/",helloWorldHandler)
http.ListenAndServe(":8000",nil)
}

Build the Go app so it can be run:

gobuild.

A binary called go-hello-world is created in the current directory. This binary is only needed for local testing, as Rockcraft will compile the Go app when we pack the rock.

Let’s Run the Go app to verify that it works:

./go-hello-world

The app starts an HTTP server listening on port 8000 that we can test by using curl to send a request to the root endpoint. We’ll need a new shell of the VM for this – in a separate terminal, run multipass shell rock-dev again:

curl--faillocalhost:8000

The Go app should respond with Hello, world!.

The Go app looks good, so let’s close the terminal instance we used for testing and stop the app in the original terminal instance by pressing Ctrl + C.

Pack the Go app into a rock

Now let’s create a container image for our Go app. We’ll use a rock, which is an OCI-compliant container image based on Ubuntu.

First, we’ll need a rockcraft.yaml project file. We’ll take advantage of a pre-defined extension in Rockcraft with the --profile flag that caters initial rock files for specific web app frameworks. Using the Go profile, Rockcraft automates the creation of rockcraft.yaml and tailors the file for a Go app. From the ~/go-hello-world directory, initialize the rock:

rockcraftinit--profilego-framework

The rockcraft.yaml file will automatically be created and set the name based on your working directory.

Check out the contents of rockcraft.yaml:

catrockcraft.yaml

The top of the file should look similar to the following snippet:

~/go-hello-world/rockcraft.yaml
name:go-hello-world
# see https://documentation.ubuntu.com/rockcraft/latest/explanation/bases/
# for more information about bases and using 'bare' bases for chiseled rocks
base:bare# as an alternative, an ubuntu base can be used
build-base:ubuntu@24.04# build-base is required when the base is bare
version:'0.1'# just for humans. Semantic versioning is recommended
summary:A summary of your Go app# 79 char long summary
description:|
This is go-hello-world's description. You have a paragraph or two to tell the
most important story about it. Keep it under 100 words though,
we live in tweetspace and your description wants to look good in the
container registries out there.
# the platforms this rock should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
amd64:
# arm64:
# ppc64el:
# s390x:

Verify that the name is go-hello-world.

The platforms key must match the architecture of your host. Check the architecture of your system:

dpkg--print-architecture

Edit the platforms key in rockcraft.yaml if required.

Note

For this tutorial, we name the rock go-hello-world and assume we are running on an amd64 platform. Check the architecture of the system using dpkg --print-architecture.

The name, version and platform all influence the name of the generated .rock file.

Pack the rock:

rockcraftpack

Once Rockcraft has finished packing the Go rock, we’ll find a new file in the working directory (an OCI image) with the .rock extension:

ls*.rock-l--block-size=MB

Run the Go rock with Docker

We already have the rock as an OCI archive. Now we need to load it into Docker. Docker requires rocks to be imported into the daemon since they can’t be run directly like an executable.

Copy the rock:

rockcraft.skopeocopy\
--insecure-policy\
oci-archive:go-hello-world_0.1_$(dpkg--print-architecture).rock\
docker-daemon:go-hello-world:0.1

This command contains the following pieces:

  • --insecure-policy: adopts a permissive policy that removes the need for a dedicated policy file.

  • oci-archive: specifies the rock we created for our Go app.

  • docker-daemon: specifies the name of the image in the Docker registry.

Check that the image was successfully loaded into Docker:

dockerimagesgo-hello-world:0.1

The output should list the Go container image, along with its tag, ID and size:

ubuntu@rock-dev:~/go-hello-world$
docker images go-hello-world:0.1
REPOSITORY TAG IMAGE ID CREATED SIZE
go-hello-world 0.1 f3abf7ebc169 5 minutes ago 15.7MB

Now we’re finally ready to run the rock and test the containerised Go app:

docker run --rm -d -p 8000:8000 \
 --name go-hello-world go-hello-world:0.1

Use the same curl command as before to send a request to the Go app’s root endpoint which is running inside the container:

curl --fail localhost:8000

The Go app again responds with Hello, world!.

View the app logs

When deploying the Go rock, we can always get the app logs with Pebble:

docker exec go-hello-world pebble logs go

As a result, Pebble will give the logs for the Go service running inside the container. We should expect to see something similar to this:

ubuntu@rock-dev:~/go-hello-world$
docker exec go-hello-world pebble logs go
2024年10月04日T08:51:35.826Z [go] 2024年10月04日 08:51:35 starting hello world application
2024年10月04日T08:51:39.974Z [go] 2024年10月04日 08:51:39 new hello world request

We can also choose to follow the logs by using the -f option with the pebble logs command above. To stop following the logs, press Ctrl + C.

Stop the app

Now we have a fully functional rock for a Go app! This concludes the first part of this tutorial, so we’ll stop the container and remove the respective image for now:

dockerstopgo-hello-world
dockerrmigo-hello-world:0.1

Update the Go app

As a final step, let’s update our app. For example, we want to add a new /time endpoint which returns the current time in UTC.

Start by opening the main.go file in a text editor and update the code to look like the following:

~/go-hello-world/main.go
packagemain
import(
"fmt"
"log"
"net/http"
"time"
)
funchelloWorldHandler(whttp.ResponseWriter,req*http.Request){
log.Printf("new hello world request")
fmt.Fprintln(w,"Hello, world!")
}
functimeHandler(whttp.ResponseWriter,req*http.Request){
log.Printf("new time request")
now:=time.Now()
fmt.Fprintln(w,now.Format(time.DateTime))
}
funcmain(){
log.Printf("starting hello world application")
http.HandleFunc("/",helloWorldHandler)
http.HandleFunc("/time",timeHandler)
http.ListenAndServe(":8000",nil)
}

Since we are creating a new version of the app, open the project file and set version: '0.2'. The top of the rockcraft.yaml file should look similar to the following:

~/go-hello-world/rockcraft.yaml
name:go-hello-world
# see https://documentation.ubuntu.com/rockcraft/latest/explanation/bases/
# for more information about bases and using 'bare' bases for chiseled rocks
base:bare# as an alternative, an ubuntu base can be used
build-base:ubuntu@24.04# build-base is required when the base is bare
version:'0.2'
summary:A summary of your Go app# 79 char long summary
description:|
This is go-hello-world's description. You have a paragraph or two to tell the
most important story about it. Keep it under 100 words though,
we live in tweetspace and your description wants to look good in the
container registries out there.
# the platforms this rock should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
amd64:
# arm64:
# ppc64el:
# s390x:

Note

If we repack the rock without changing the version, the new rock will have the same name and overwrite the last one we built. It’s a good practice to change the version whenever we make changes to the app in the image.

Pack and run the rock using similar commands as before:

rockcraft pack
rockcraft.skopeo --insecure-policy \
 copy oci-archive:go-hello-world_0.2_$(dpkg --print-architecture).rock \
 docker-daemon:go-hello-world:0.2
docker images go-hello-world:0.2
docker run --rm -d -p 8000:8000 \
 --name go-hello-world go-hello-world:0.2

Note

Note that the resulting .rock file will now be named differently, as its new version will be part of the filename.

Finally, use curl to send a request to the /time endpoint:

curl --fail localhost:8000/time

The updated app will respond with the current date and time in UTC.

Note

If we are not getting the current date and time from the /time endpoint, check the Troubleshooting steps below.

Cleanup

We can now stop the container and remove the corresponding image:

dockerstopgo-hello-world
dockerrmigo-hello-world:0.2

Reset the environment

We’ve reached the end of this tutorial.

If we’d like to reset the working environment, we can simply run the following:

# delete all the files created during the tutorial
rmgo.modrockcraft.yamlmain.gogo-hello-world\
go-hello-world_0.1_$(dpkg--print-architecture).rock\
go-hello-world_0.2_$(dpkg--print-architecture).rock

We can also clean the Multipass instance up. Start by exiting it:

exit

And then we can proceed with its deletion:

multipassdeleterock-dev
multipasspurge

Next steps

Congratulations! You’ve reached the end of this tutorial. You created a Go app, packaged it into a rock, and practiced some typical development skills such as viewing logs and updating the app.

But there is a lot more to explore:


Troubleshooting

App updates not taking effect?

Upon changing the Go app and re-packing the rock, if the changes are not taking effect, try running rockcraft clean and pack the rock again with rockcraft pack.