Skip to content

Commit ecf6a9b

Browse files
committed
New Shipping Apps (using docker and snapcraft) section
1 parent 67dbe76 commit ecf6a9b

File tree

4 files changed

+277
-0
lines changed

4 files changed

+277
-0
lines changed

SUMMARY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,6 @@
130130
* [Writing Shards](guides/writing_shards.md)
131131
* [Hosting on GitHub](guides/hosting/github.md)
132132
* [Hosting on GitLab](guides/hosting/gitlab.md)
133+
* [Shipping Apps](guides/shipping.md)
134+
* [Using Docker](guides/shipping/docker.md)
135+
* [Using Snapcraft](guides/shipping/snapcraft.md)

guides/shipping.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Shipping
2+
3+
_Ahoy, matey!_
4+
Oh! you've built the greatest app ever and, of course, using Crystal!!
5+
And now you want to share it with the whole world?!
6+
7+
Well, it’s your lucky ~~pirate~~ day because we are going to ship our first Crystal application!!
8+
9+
So, _weigh anchor and hoist the mizzen! Yarr!_
10+
11+
## The App
12+
13+
The application we are shipping is an example of a static file sharing server. Here’s the source code:
14+
15+
```crystal
16+
# staticserver.cr
17+
require "http"
18+
require "option_parser"
19+
20+
# Handle Ctrl+C and kill signal.
21+
# Needed for hosting this process in a docker
22+
# as the entry point command
23+
Signal::INT.trap { puts "Caught Ctrl+C..."; exit }
24+
Signal::TERM.trap { puts "Caught kill..."; exit }
25+
26+
path = "/www"
27+
port = 80
28+
29+
option_parser = OptionParser.parse do |parser|
30+
parser.on "-f PATH", "--files=PATH", "Files path (default: /www)" do |files_path|
31+
path = files_path
32+
end
33+
parser.on "-p PORT", "--port=PORT", "Port to listen (default: 80)" do |server_port|
34+
port = server_port.to_i
35+
end
36+
end
37+
38+
server = HTTP::Server.new([
39+
HTTP::LogHandler.new,
40+
HTTP::ErrorHandler.new,
41+
HTTP::StaticFileHandler.new(path),
42+
])
43+
44+
address = server.bind_tcp "0.0.0.0", port
45+
puts "Listening on http://#{address} and serving files in path #{path}"
46+
server.listen
47+
```
48+
49+
So, starting the server (listening on port 8080 and serving files under the current directory) is as easy as running:
50+
51+
```shell-session
52+
$ crystal ./src/staticserver.cr -- -p 8080 -f .
53+
Listening on http://0.0.0.0:8080 and serving files in path .
54+
```
55+
56+
**Note:** the default behavior is to listen on `port 80` and serve the folder `/www`.
57+
58+
## Compiling our application
59+
60+
Let’s go over Crystal’s [introduction](https://crystal-lang.org/reference/). One of the main goals of the language is to _Compile to efficient native code_. That means that each time that we compile our code then an executable is built, but with an important property: it has a target platform (architecture and operating system), which is where the application will run. Crystal knows the target platform because is the same as the one being used to compile.
61+
For example, if we use a Linux OS based computer for compiling, then the executable will be meant to run on a Linux OS (and in some cases we will need to use the same Linux distribution).
62+
63+
Can we set the target when calling the compiler? Oh, that’s a great idea, but for now it’s not an option (there are a lot of great buccanears [working on a solution](https://forum.crystal-lang.org/t/cross-compiling-automatically-to-osx/1330/12) and remember that Crystal is open source: so you are welcome aboard!)
64+
65+
Let’s compile our application:
66+
67+
```shell-session
68+
$ shards build --production
69+
Dependencies are satisfied
70+
Building: staticserver
71+
```
72+
73+
and now, if we want to know the file type:
74+
75+
If we are using Mac OS, we will see something like this:
76+
77+
```shell-session
78+
$ file bin/staticserver
79+
bin/staticserver: Mach-O 64-bit executable x86_64
80+
```
81+
82+
And if we are using a Linux distribution, for example Ubuntu:
83+
84+
```shell-session
85+
$ file bin/staticserver
86+
bin/staticserver: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=a78ffe59325c3f9668d551852e7717e3996edb3b, not stripped
87+
```
88+
89+
90+
Furthermore, our application may use some libraries (our application’s dependencies!) and so the target platform should have this libraries installed. To list the libraries used by our application:
91+
92+
On Mac OS:
93+
94+
```shell-session
95+
$ otool -L ./bin/staticserver
96+
./bin/staticserver:
97+
/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.11)
98+
/usr/local/opt/openssl/lib/libssl.1.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
99+
/usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
100+
/usr/lib/libpcre.0.dylib (compatibility version 1.0.0, current version 1.1.0)
101+
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.250.1)
102+
/usr/local/opt/libevent/lib/libevent-2.1.7.dylib (compatibility version 8.0.0, current version 8.0.0)
103+
/usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0)
104+
```
105+
106+
On a Linux OS we may use `ldd bin/staticserver` with a similar result.
107+
108+
Up to this point, we know that for shipping our application, we need to compile for each target platform where we want our application to run; and also, we need to provide the dependencies used by our application.
109+
110+
Here we will see two ways for shipping our application:
111+
112+
* Using a [Docker](https://www.docker.com/get-started) image.
113+
* Using a [Snapcraft](https://snapcraft.io/build) package.
114+
115+
116+
## Shipping with Docker
117+
118+
The idea behind using Docker is to create a Docker container, with a target platform, and use it for building our application and then create a really small image for shipping and running our application!
119+
120+
Wow! I want to [embark on this adventure](./shipping/docker.html)!
121+
122+
## Shipping with Snapcraft
123+
124+
The idea behind using Snapcraft is to use this tool for building an executable targeting the Linux OS and then publishing it!
125+
126+
Oh great! Let’s follow [this sea lane](./shipping/snapcraft.html)!

guides/shipping/docker.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Shipping with Docker
2+
3+
Already at the docks?! Not at all, matey, we’ve just started the journey! Docker will let us ship our application in a more homogeneous way.
4+
5+
We will create a `Dockerfile` that will let us:
6+
* create a base image
7+
* build the application
8+
* list the dependencies
9+
* build a custom docker image (only with our application and the dependencies) based on a small docker image.
10+
11+
And then we will publish the final image (with our application)
12+
13+
## Creating the Dockerfile
14+
15+
Let’s create a `Dockerfile`. This file will create a docker image from other image: `crystallang/crystal:latest`, which is based on Ubuntu and it ships with the Crystal compiler. On the other hand, we will use multistage-build, so that our new image will be as small as possible (brave buccaneers have [already sailed](https://manas.tech/blog/2017/04/03/shipping-crystal-apps-in-a-small-docker-image/) these [wild seas](https://gist.github.com/bcardiff/85ae47e66ff0df35a78697508fcb49af)).
16+
17+
Our `Dockerfile` will look like this:
18+
19+
```dockerfile
20+
FROM crystallang/crystal:latest
21+
22+
ADD . /src
23+
WORKDIR /src
24+
RUN shards build --production
25+
26+
RUN ldd bin/staticserver | tr -s '[:blank:]' '\n' | grep '^/' | \
27+
xargs -I % sh -c 'mkdir -p $(dirname deps%); cp % deps%;'
28+
29+
FROM scratch
30+
COPY --from=0 /src/deps /
31+
COPY --from=0 /src/bin/staticserver /staticserver
32+
33+
EXPOSE 80
34+
35+
ENTRYPOINT ["/staticserver"]
36+
```
37+
38+
**Note:** if you are building an application that needs static files (for example: you are building a Web Application and you need `favicon.ico`), in that case we would need to copy those files to the final image using `COPY`.
39+
40+
Let’s build it with:
41+
42+
```shell-session
43+
$ docker build -t "staticserver:0.1.0" .
44+
```
45+
46+
Was our image created? Well, I hope so (or someone will be _walking the plank_)
47+
To be certain, let’s run:
48+
49+
```shell-session
50+
$ docker images
51+
REPOSITORY TAG IMAGE ID CREATED SIZE
52+
staticserver 0.1.0 0b57eeef751c 9 seconds ago 10.4MB
53+
```
54+
55+
Sink Me!! Only 10.4MB! This is great!
56+
57+
**Why are we listing the dependencies inside the container?**
58+
59+
We are doing this because the dependencies would be different depending on the Operating System the application is running (or will run). In this case we need the dependencies for an Ubuntu distribution.
60+
61+
**And why are we using multistage-build?**
62+
63+
Well, first we need the Crystal compiler for building our application, so we base our image on the `crystallang/crystal:latest`image.
64+
Then we won’t need the compiler anymore, so we base the final image on Docker Official Image [scratch](https://hub.docker.com/_/scratch/). In case the `scratch` image is not enough then we may use another image based on Ubuntu since the Crystal image is based on this Linux distribution.
65+
66+
**Wait! and if I’m using an external library like `sqlite`?**
67+
68+
Oh well, in that case we are going to need it for compiling and then the script should make the external library available in the final image.
69+
70+
Continuing our example, before publishing our image let’s see if it’s working:
71+
72+
```shell-session
73+
$ docker run --rm -it -v ${PWD}:/www -p 8080:80 staticserver:0.1.0
74+
Listening on http://0.0.0.0:80 and serving files in path /www
75+
```
76+
77+
If we go to our browser and navigate to `http://localhost:8080` then we will see the files list. Yarr!
78+
79+
### Publishing
80+
81+
Finally, we only have to publish our new image with our application!
82+
To do so, we may use [`docker push`](https://docs.docker.com/engine/reference/commandline/push/) to push the image to a registry (for example [Docker Hub](https://hub.docker.com/))

guides/shipping/snapcraft.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Shipping with snapcraft.io
2+
3+
Snapcraft will allow us to package and publish our application for users using Linux. Also, it will allow the users to discover our application more easily through the [Snapcraft Store](https://snapcraft.io/store)
4+
We only need to create a configuration file where we set the language, the application and dependencies. So let’s start!
5+
6+
## Creating the snap
7+
8+
As the [snapcraft documentation](https://snapcraft.io/docs) says _Snaps are app packages for desktop, cloud and IoT that are easy to install, secure, cross-platform and dependency-free_ And also important: they are **containerised** software.
9+
10+
To describe our application, Snapcraft uses a [snapcraft.yaml](https://snapcraft.io/docs/snapcraft-yaml-reference) file. In our example, the file `snapcraft.yaml` will be located in a folder called `snap` and it will look like this:
11+
12+
```yaml
13+
name: crystal-staticserver
14+
version: "0.1.0"
15+
summary: Create the static file server snap
16+
description: Create the static file server snap
17+
18+
base: core
19+
grade: devel
20+
confinement: classic
21+
build-packages:
22+
- libz-dev
23+
- libssl-dev
24+
25+
apps:
26+
crystal-staticserver:
27+
command: bin/staticserver
28+
29+
parts:
30+
crystal-staticserver:
31+
plugin: crystal
32+
source: ./
33+
```
34+
35+
Let’s see some of the fields:
36+
37+
`name`, `version`, `summary` and `description` define and describe our application, allowing users to easily find software in the store.
38+
39+
The `base` field will let us specify a [base snap](https://snapcraft.io/docs/base-snaps) which will _provide a run-time environment with a minimal set of libraries_.
40+
In our example `base: core` is based on `Ubuntu 16.04 LTS`
41+
42+
The `grade` field could be `stable` or `devel` (for development). This will have an impact on the [channels](https://snapcraft.io/docs/channels) where our application could be published.
43+
44+
The `confinement`field will set the [degree of isolation](https://snapcraft.io/docs/snap-confinement)
45+
46+
In the `build-packages` field we may list needed libraries for building our application.
47+
48+
In the `apps` field we will set `app-name` and the `command` to run it.
49+
50+
The `parts` field will let us define the differents [building blocks](https://snapcraft.io/docs/snapcraft-parts-metadata) that form our application.
51+
Here, in the `plugin` field we may set the tool that will drive the building process. In our example we will use the [crystal plugin](https://snapcraft.io/docs/the-crystal-plugin)
52+
53+
Great! Now, we only need to build the package, using:
54+
55+
```shell-session
56+
$ snapcraft
57+
Launching a VM.
58+
Starting snapcraft-crystal-staticserver -
59+
...
60+
Snapping 'crystal-staticserver' \
61+
Snapped crystal-staticserver_0.1.0_amd64.snap
62+
```
63+
64+
## Publishing
65+
66+
Finally, to share our application to the world, we need to publish it in the Snapcraft Store. Follow the steps described in the [official documentation](https://snapcraft.io/docs/releasing-your-app)

0 commit comments

Comments
 (0)