├── .gitignore ├── CHANGES.txt ├── MANIFEST.in ├── README.md ├── docker_tutorial ├── __init__.py ├── admin.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto__add_subscriber__add_unique_subscriber_email_from_level__add_dock.py │ ├── 0003_auto__add_field_tutorialuser_http_user_agent__add_field_tutorialuser_h.py │ ├── 0004_auto__del_field_tutorialuser_http_accept_encoding__add_field_tutorialu.py │ ├── 0005_auto__chg_field_tutorialuser_http_user_agent__chg_field_tutorialuser_h.py │ └── __init__.py ├── models.py ├── static │ ├── css │ │ ├── tutorial-style.css │ │ └── tutorial-style.less │ ├── img │ │ ├── circle-green.svg │ │ ├── docker-letters.png │ │ ├── docker-topicon.png │ │ ├── email.png │ │ ├── facebook.png │ │ ├── favicon.png │ │ ├── fullscreen_exit_32x32.png │ │ └── twitter.png │ ├── js │ │ ├── steps.coffee │ │ ├── steps.js │ │ ├── terminal.coffee │ │ └── terminal.js │ └── lib │ │ ├── css │ │ └── jquery.terminal.css │ │ └── js │ │ ├── jquery-1.7.1.min.js │ │ ├── jquery.mousewheel-min.js │ │ ├── jquery.terminal-0.7.4.js │ │ ├── jquery.terminal-0.7.4.min.js │ │ └── sugar-1.3.9.min.js ├── templates │ └── tutorial │ │ ├── snippet.html │ │ ├── stats.html │ │ └── testpage.html ├── tests │ ├── __init__.py │ └── test_views.py ├── urls.py ├── utils.py └── views.py ├── dockerfile_tutorial ├── __init__.py ├── static │ ├── css │ │ └── dockerfile_tutorial.css │ └── js │ │ ├── dockerfile_tutorial.js │ │ ├── dockerfile_tutorial_level1.js │ │ ├── dockerfile_tutorial_level2.js │ │ └── jquery.cookie.js ├── templates │ └── dockerfile │ │ ├── _dockerfile.html │ │ ├── introduction.html │ │ ├── level1.html │ │ └── level2.html └── urls.py ├── dotcloud.yml ├── requirements.txt ├── runtests.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .coverage 3 | *.pyc 4 | DockerTutorial.egg-info 5 | MANIFEST 6 | dist/ 7 | htmlcov 8 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | 0.1, Aug 13 2013 -- Initial release 2 | The initial release of the Docker tutorial 3 | 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include README.md 3 | recursive-include docker_tutorial/static * 4 | recursive-include docker_tutorial/templates * 5 | 6 | recursive-include dockerfile_tutorial/static * 7 | recursive-include dockerfile_tutorial/templates * 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Docker Tutorial 2 | =============== 3 | 4 | The Docker tutorial is an interactive learning environment to get familiar with the Docker commandline. 5 | 6 | 7 | Simple Usage 8 | ------------ 9 | 10 | Generally this application is used in the www.docker.io website. It's source can be found on 11 | https://github.com/dotcloud/www.docker.io/. By installing that you will get this app 'for free' as a 12 | dependency. 13 | 14 | However, this app is made to also be usable inside other Django applications. All that is required is to 15 | add git+https://github.com/dhrp/docker-tutorial.git#egg=DockerTutorial-dev to your requirements.txt 16 | and it will be installed by pip upon pip install -r requirements.txt 17 | 18 | To include it 19 | * Make sure your "host" app uses the same python environment 20 | * In your "host" app settings include "docker_tutorial" 21 | * in a template {% include 'tutorial/snippet.html' %} 22 | * in your urls.py add url(r'^tutorial/', include('docker_tutorial.urls')), 23 | * in your settings make sure you include the session middleware: 24 | 25 | 26 | When you want to make changes 27 | ----------------------------- 28 | 29 | * First create or switch to a virtual environment in which you have the "host" app into which you would 30 | like to embed the tutorial. e.g. a clone of the the Docker website (before you ran install) 31 | * Clone this repository: 32 | git clone https://github.com/dhrp/docker-tutorial.git 33 | * Switch to the dir: 34 | cd docker-tutorial 35 | * Install the application with the -e (editable) flag. 36 | pip install -e . 37 | 38 | This will setup the symlinks such that you don't need to run setup.py every time you want to see a 39 | change. i.e. your local repository is now linked into the environment. 40 | 41 | Running the unit tests 42 | ---------------------- 43 | 44 | * ./runtests.py 45 | 46 | Running code coverage 47 | --------------------- 48 | 49 | * coverage run ./runtests.py && coverage html --include="./docker_tutorial/*" 50 | 51 | Happy coding! -------------------------------------------------------------------------------- /docker_tutorial/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'thatcher' 2 | -------------------------------------------------------------------------------- /docker_tutorial/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.sessions.models import Session 3 | from .models import * 4 | 5 | 6 | class SessionAdmin(admin.ModelAdmin): 7 | def _session_data(self, obj): 8 | return obj.get_decoded() 9 | list_display = ['session_key', '_session_data', 'expire_date'] 10 | admin.site.register(Session, SessionAdmin) 11 | 12 | 13 | class TutorialEventInline(admin.TabularInline): 14 | model = TutorialEvent 15 | readonly_fields = ('timestamp', 'time_elapsed', 'question', 'type', 'command',) 16 | fields = ['question', 'type', 'command', 'timestamp', 'time_elapsed'] 17 | extra = 0 18 | 19 | 20 | class DockerfileEvenInline(admin.TabularInline): 21 | model = DockerfileEvent 22 | readonly_fields = ('timestamp', 'level', 'item', 'errors', ) 23 | fields = ['level', 'item', 'errors', 'timestamp'] 24 | extra = 0 25 | 26 | 27 | class TutorialUserAdmin(admin.ModelAdmin): 28 | model = TutorialUser 29 | list_display = ['id', 'session_key', 'label', 'timestamp'] 30 | inlines = [TutorialEventInline, DockerfileEvenInline] 31 | admin.site.register(TutorialUser, TutorialUserAdmin) 32 | 33 | 34 | class TutorialEventAdmin(admin.ModelAdmin): 35 | model = TutorialEvent 36 | readonly_fields = ('id', 'timestamp', 'time_elapsed') 37 | fields = ['id', 'user', 'type', 'question', 'command', 'timestamp', 'time_elapsed'] 38 | list_display = ['id', 'user','type', 'question', 'timestamp'] 39 | admin.site.register(TutorialEvent, TutorialEventAdmin) 40 | 41 | 42 | class TutorialEventFeedback(TutorialEvent): 43 | class Meta: 44 | proxy = True 45 | 46 | 47 | class TutorialEventFeedbackAdmin(admin.ModelAdmin): 48 | model = TutorialEvent 49 | readonly_fields = ('id', 'timestamp', 'time_elapsed') 50 | list_display = ['id', 'user', 'question', 'feedback', 'timestamp'] 51 | list_filter = ('type',) 52 | admin.site.register(TutorialEventFeedback, TutorialEventFeedbackAdmin) 53 | 54 | 55 | class SubscriberAdmin(admin.ModelAdmin): 56 | model = Subscriber 57 | list_display = ['email', 'from_level'] 58 | admin.site.register(Subscriber, SubscriberAdmin) 59 | 60 | 61 | class DockerfileEventAdmin(admin.ModelAdmin): 62 | model = DockerfileEvent 63 | list_display = ['id', 'timestamp', 'item', 'level', 'errors'] 64 | admin.site.register(DockerfileEvent, DockerfileEventAdmin) -------------------------------------------------------------------------------- /docker_tutorial/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'TutorialUser' 12 | db.create_table(u'docker_tutorial_tutorialuser', ( 13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('session_key', self.gf('django.db.models.fields.CharField')(max_length=80)), 15 | ('timestamp', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, auto_now=True, blank=True)), 16 | ('label', self.gf('django.db.models.fields.CharField')(default='', max_length=80, null=True, blank=True)), 17 | )) 18 | db.send_create_signal(u'docker_tutorial', ['TutorialUser']) 19 | 20 | # Adding model 'TutorialEvent' 21 | db.create_table(u'docker_tutorial_tutorialevent', ( 22 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 23 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['docker_tutorial.TutorialUser'])), 24 | ('type', self.gf('django.db.models.fields.CharField')(max_length=15)), 25 | ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), 26 | ('question', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), 27 | ('command', self.gf('django.db.models.fields.CharField')(default='', max_length=80, blank=True)), 28 | ('feedback', self.gf('django.db.models.fields.CharField')(default='', max_length=2000, blank=True)), 29 | )) 30 | db.send_create_signal(u'docker_tutorial', ['TutorialEvent']) 31 | 32 | 33 | def backwards(self, orm): 34 | # Deleting model 'TutorialUser' 35 | db.delete_table(u'docker_tutorial_tutorialuser') 36 | 37 | # Deleting model 'TutorialEvent' 38 | db.delete_table(u'docker_tutorial_tutorialevent') 39 | 40 | 41 | models = { 42 | u'docker_tutorial.tutorialevent': { 43 | 'Meta': {'object_name': 'TutorialEvent'}, 44 | 'command': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80', 'blank': 'True'}), 45 | 'feedback': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '2000', 'blank': 'True'}), 46 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 47 | 'question': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 48 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 49 | 'type': ('django.db.models.fields.CharField', [], {'max_length': '15'}), 50 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docker_tutorial.TutorialUser']"}) 51 | }, 52 | u'docker_tutorial.tutorialuser': { 53 | 'Meta': {'object_name': 'TutorialUser'}, 54 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'label': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80', 'null': 'True', 'blank': 'True'}), 56 | 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '80'}), 57 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}) 58 | } 59 | } 60 | 61 | complete_apps = ['docker_tutorial'] -------------------------------------------------------------------------------- /docker_tutorial/migrations/0002_auto__add_subscriber__add_unique_subscriber_email_from_level__add_dock.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'Subscriber' 12 | db.create_table(u'docker_tutorial_subscriber', ( 13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('email', self.gf('django.db.models.fields.EmailField')(max_length=80)), 15 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['docker_tutorial.TutorialUser'])), 16 | ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), 17 | ('from_level', self.gf('django.db.models.fields.IntegerField')()), 18 | )) 19 | db.send_create_signal(u'docker_tutorial', ['Subscriber']) 20 | 21 | # Adding unique constraint on 'Subscriber', fields ['email', 'from_level'] 22 | db.create_unique(u'docker_tutorial_subscriber', ['email', 'from_level']) 23 | 24 | # Adding model 'DockerfileEvent' 25 | db.create_table(u'docker_tutorial_dockerfileevent', ( 26 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 27 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['docker_tutorial.TutorialUser'])), 28 | ('item', self.gf('django.db.models.fields.CharField')(max_length=15)), 29 | ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), 30 | ('level', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), 31 | ('errors', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), 32 | )) 33 | db.send_create_signal(u'docker_tutorial', ['DockerfileEvent']) 34 | 35 | 36 | def backwards(self, orm): 37 | # Removing unique constraint on 'Subscriber', fields ['email', 'from_level'] 38 | db.delete_unique(u'docker_tutorial_subscriber', ['email', 'from_level']) 39 | 40 | # Deleting model 'Subscriber' 41 | db.delete_table(u'docker_tutorial_subscriber') 42 | 43 | # Deleting model 'DockerfileEvent' 44 | db.delete_table(u'docker_tutorial_dockerfileevent') 45 | 46 | 47 | models = { 48 | u'docker_tutorial.dockerfileevent': { 49 | 'Meta': {'object_name': 'DockerfileEvent'}, 50 | 'errors': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 51 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 52 | 'item': ('django.db.models.fields.CharField', [], {'max_length': '15'}), 53 | 'level': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 54 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 55 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docker_tutorial.TutorialUser']"}) 56 | }, 57 | u'docker_tutorial.subscriber': { 58 | 'Meta': {'unique_together': "(('email', 'from_level'),)", 'object_name': 'Subscriber'}, 59 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '80'}), 60 | 'from_level': ('django.db.models.fields.IntegerField', [], {}), 61 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 63 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docker_tutorial.TutorialUser']"}) 64 | }, 65 | u'docker_tutorial.tutorialevent': { 66 | 'Meta': {'object_name': 'TutorialEvent'}, 67 | 'command': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80', 'blank': 'True'}), 68 | 'feedback': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '2000', 'blank': 'True'}), 69 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 70 | 'question': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 71 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 72 | 'type': ('django.db.models.fields.CharField', [], {'max_length': '15'}), 73 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docker_tutorial.TutorialUser']"}) 74 | }, 75 | u'docker_tutorial.tutorialuser': { 76 | 'Meta': {'object_name': 'TutorialUser'}, 77 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 78 | 'label': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80', 'null': 'True', 'blank': 'True'}), 79 | 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '80'}), 80 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}) 81 | } 82 | } 83 | 84 | complete_apps = ['docker_tutorial'] -------------------------------------------------------------------------------- /docker_tutorial/migrations/0003_auto__add_field_tutorialuser_http_user_agent__add_field_tutorialuser_h.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding field 'TutorialUser.http_user_agent' 12 | db.add_column(u'docker_tutorial_tutorialuser', 'http_user_agent', 13 | self.gf('django.db.models.fields.CharField')(default='', max_length=256, blank=True), 14 | keep_default=False) 15 | 16 | # Adding field 'TutorialUser.http_remote_address' 17 | db.add_column(u'docker_tutorial_tutorialuser', 'http_remote_address', 18 | self.gf('django.db.models.fields.CharField')(default='', max_length=32, blank=True), 19 | keep_default=False) 20 | 21 | # Adding field 'TutorialUser.http_accept_language' 22 | db.add_column(u'docker_tutorial_tutorialuser', 'http_accept_language', 23 | self.gf('django.db.models.fields.CharField')(default='', max_length=128, blank=True), 24 | keep_default=False) 25 | 26 | # Adding field 'TutorialUser.http_accept_encoding' 27 | db.add_column(u'docker_tutorial_tutorialuser', 'http_accept_encoding', 28 | self.gf('django.db.models.fields.CharField')(default='', max_length=128, blank=True), 29 | keep_default=False) 30 | 31 | # Adding field 'TutorialUser.http_referrer' 32 | db.add_column(u'docker_tutorial_tutorialuser', 'http_referrer', 33 | self.gf('django.db.models.fields.CharField')(default='', max_length=128, blank=True), 34 | keep_default=False) 35 | 36 | 37 | # Changing field 'TutorialUser.label' 38 | db.alter_column(u'docker_tutorial_tutorialuser', 'label', self.gf('django.db.models.fields.CharField')(max_length=80)) 39 | 40 | def backwards(self, orm): 41 | # Deleting field 'TutorialUser.http_user_agent' 42 | db.delete_column(u'docker_tutorial_tutorialuser', 'http_user_agent') 43 | 44 | # Deleting field 'TutorialUser.http_remote_address' 45 | db.delete_column(u'docker_tutorial_tutorialuser', 'http_remote_address') 46 | 47 | # Deleting field 'TutorialUser.http_accept_language' 48 | db.delete_column(u'docker_tutorial_tutorialuser', 'http_accept_language') 49 | 50 | # Deleting field 'TutorialUser.http_accept_encoding' 51 | db.delete_column(u'docker_tutorial_tutorialuser', 'http_accept_encoding') 52 | 53 | # Deleting field 'TutorialUser.http_referrer' 54 | db.delete_column(u'docker_tutorial_tutorialuser', 'http_referrer') 55 | 56 | 57 | # Changing field 'TutorialUser.label' 58 | db.alter_column(u'docker_tutorial_tutorialuser', 'label', self.gf('django.db.models.fields.CharField')(max_length=80, null=True)) 59 | 60 | models = { 61 | u'docker_tutorial.dockerfileevent': { 62 | 'Meta': {'object_name': 'DockerfileEvent'}, 63 | 'errors': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 64 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 65 | 'item': ('django.db.models.fields.CharField', [], {'max_length': '15'}), 66 | 'level': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 67 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 68 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docker_tutorial.TutorialUser']"}) 69 | }, 70 | u'docker_tutorial.subscriber': { 71 | 'Meta': {'unique_together': "(('email', 'from_level'),)", 'object_name': 'Subscriber'}, 72 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '80'}), 73 | 'from_level': ('django.db.models.fields.IntegerField', [], {}), 74 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 75 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 76 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docker_tutorial.TutorialUser']"}) 77 | }, 78 | u'docker_tutorial.tutorialevent': { 79 | 'Meta': {'object_name': 'TutorialEvent'}, 80 | 'command': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80', 'blank': 'True'}), 81 | 'feedback': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '2000', 'blank': 'True'}), 82 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 83 | 'question': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 84 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 85 | 'type': ('django.db.models.fields.CharField', [], {'max_length': '15'}), 86 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docker_tutorial.TutorialUser']"}) 87 | }, 88 | u'docker_tutorial.tutorialuser': { 89 | 'Meta': {'object_name': 'TutorialUser'}, 90 | 'http_accept_encoding': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}), 91 | 'http_accept_language': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}), 92 | 'http_referrer': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}), 93 | 'http_remote_address': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), 94 | 'http_user_agent': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}), 95 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 96 | 'label': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80', 'blank': 'True'}), 97 | 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '80'}), 98 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}) 99 | } 100 | } 101 | 102 | complete_apps = ['docker_tutorial'] -------------------------------------------------------------------------------- /docker_tutorial/migrations/0004_auto__del_field_tutorialuser_http_accept_encoding__add_field_tutorialu.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Deleting field 'TutorialUser.http_accept_encoding' 12 | db.delete_column(u'docker_tutorial_tutorialuser', 'http_accept_encoding') 13 | 14 | # Adding field 'TutorialUser.http_real_remote_address' 15 | db.add_column(u'docker_tutorial_tutorialuser', 'http_real_remote_address', 16 | self.gf('django.db.models.fields.CharField')(default='', max_length=32, blank=True), 17 | keep_default=False) 18 | 19 | 20 | def backwards(self, orm): 21 | # Adding field 'TutorialUser.http_accept_encoding' 22 | db.add_column(u'docker_tutorial_tutorialuser', 'http_accept_encoding', 23 | self.gf('django.db.models.fields.CharField')(default='', max_length=128, blank=True), 24 | keep_default=False) 25 | 26 | # Deleting field 'TutorialUser.http_real_remote_address' 27 | db.delete_column(u'docker_tutorial_tutorialuser', 'http_real_remote_address') 28 | 29 | 30 | models = { 31 | u'docker_tutorial.dockerfileevent': { 32 | 'Meta': {'object_name': 'DockerfileEvent'}, 33 | 'errors': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 34 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 35 | 'item': ('django.db.models.fields.CharField', [], {'max_length': '15'}), 36 | 'level': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 37 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 38 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docker_tutorial.TutorialUser']"}) 39 | }, 40 | u'docker_tutorial.subscriber': { 41 | 'Meta': {'unique_together': "(('email', 'from_level'),)", 'object_name': 'Subscriber'}, 42 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '80'}), 43 | 'from_level': ('django.db.models.fields.IntegerField', [], {}), 44 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 45 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 46 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docker_tutorial.TutorialUser']"}) 47 | }, 48 | u'docker_tutorial.tutorialevent': { 49 | 'Meta': {'object_name': 'TutorialEvent'}, 50 | 'command': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80', 'blank': 'True'}), 51 | 'feedback': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '2000', 'blank': 'True'}), 52 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 53 | 'question': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 54 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 55 | 'type': ('django.db.models.fields.CharField', [], {'max_length': '15'}), 56 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docker_tutorial.TutorialUser']"}) 57 | }, 58 | u'docker_tutorial.tutorialuser': { 59 | 'Meta': {'object_name': 'TutorialUser'}, 60 | 'http_accept_language': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}), 61 | 'http_real_remote_address': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), 62 | 'http_referrer': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}), 63 | 'http_remote_address': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), 64 | 'http_user_agent': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}), 65 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 66 | 'label': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80', 'blank': 'True'}), 67 | 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '80'}), 68 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}) 69 | } 70 | } 71 | 72 | complete_apps = ['docker_tutorial'] -------------------------------------------------------------------------------- /docker_tutorial/migrations/0005_auto__chg_field_tutorialuser_http_user_agent__chg_field_tutorialuser_h.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | 12 | # Changing field 'TutorialUser.http_user_agent' 13 | db.alter_column(u'docker_tutorial_tutorialuser', 'http_user_agent', self.gf('django.db.models.fields.TextField')()) 14 | 15 | # Changing field 'TutorialUser.http_real_remote_address' 16 | db.alter_column(u'docker_tutorial_tutorialuser', 'http_real_remote_address', self.gf('django.db.models.fields.TextField')()) 17 | 18 | # Changing field 'TutorialUser.http_remote_address' 19 | db.alter_column(u'docker_tutorial_tutorialuser', 'http_remote_address', self.gf('django.db.models.fields.TextField')()) 20 | 21 | # Changing field 'TutorialUser.http_accept_language' 22 | db.alter_column(u'docker_tutorial_tutorialuser', 'http_accept_language', self.gf('django.db.models.fields.TextField')()) 23 | 24 | # Changing field 'TutorialUser.http_referrer' 25 | db.alter_column(u'docker_tutorial_tutorialuser', 'http_referrer', self.gf('django.db.models.fields.TextField')()) 26 | 27 | # Changing field 'TutorialUser.session_key' 28 | db.alter_column(u'docker_tutorial_tutorialuser', 'session_key', self.gf('django.db.models.fields.CharField')(unique=True, max_length=40)) 29 | # Adding unique constraint on 'TutorialUser', fields ['session_key'] 30 | db.create_unique(u'docker_tutorial_tutorialuser', ['session_key']) 31 | 32 | 33 | def backwards(self, orm): 34 | # Removing unique constraint on 'TutorialUser', fields ['session_key'] 35 | db.delete_unique(u'docker_tutorial_tutorialuser', ['session_key']) 36 | 37 | 38 | # Changing field 'TutorialUser.http_user_agent' 39 | db.alter_column(u'docker_tutorial_tutorialuser', 'http_user_agent', self.gf('django.db.models.fields.CharField')(max_length=256)) 40 | 41 | # Changing field 'TutorialUser.http_real_remote_address' 42 | db.alter_column(u'docker_tutorial_tutorialuser', 'http_real_remote_address', self.gf('django.db.models.fields.CharField')(max_length=32)) 43 | 44 | # Changing field 'TutorialUser.http_remote_address' 45 | db.alter_column(u'docker_tutorial_tutorialuser', 'http_remote_address', self.gf('django.db.models.fields.CharField')(max_length=32)) 46 | 47 | # Changing field 'TutorialUser.http_accept_language' 48 | db.alter_column(u'docker_tutorial_tutorialuser', 'http_accept_language', self.gf('django.db.models.fields.CharField')(max_length=128)) 49 | 50 | # Changing field 'TutorialUser.http_referrer' 51 | db.alter_column(u'docker_tutorial_tutorialuser', 'http_referrer', self.gf('django.db.models.fields.CharField')(max_length=128)) 52 | 53 | # Changing field 'TutorialUser.session_key' 54 | db.alter_column(u'docker_tutorial_tutorialuser', 'session_key', self.gf('django.db.models.fields.CharField')(max_length=80)) 55 | 56 | models = { 57 | u'docker_tutorial.dockerfileevent': { 58 | 'Meta': {'object_name': 'DockerfileEvent'}, 59 | 'errors': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 60 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 61 | 'item': ('django.db.models.fields.CharField', [], {'max_length': '15'}), 62 | 'level': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 63 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 64 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docker_tutorial.TutorialUser']"}) 65 | }, 66 | u'docker_tutorial.subscriber': { 67 | 'Meta': {'unique_together': "(('email', 'from_level'),)", 'object_name': 'Subscriber'}, 68 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '80'}), 69 | 'from_level': ('django.db.models.fields.IntegerField', [], {}), 70 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 71 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 72 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docker_tutorial.TutorialUser']"}) 73 | }, 74 | u'docker_tutorial.tutorialevent': { 75 | 'Meta': {'object_name': 'TutorialEvent'}, 76 | 'command': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80', 'blank': 'True'}), 77 | 'feedback': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '2000', 'blank': 'True'}), 78 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 79 | 'question': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 80 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 81 | 'type': ('django.db.models.fields.CharField', [], {'max_length': '15'}), 82 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['docker_tutorial.TutorialUser']"}) 83 | }, 84 | u'docker_tutorial.tutorialuser': { 85 | 'Meta': {'object_name': 'TutorialUser'}, 86 | 'http_accept_language': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), 87 | 'http_real_remote_address': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), 88 | 'http_referrer': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), 89 | 'http_remote_address': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), 90 | 'http_user_agent': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), 91 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 92 | 'label': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '80', 'blank': 'True'}), 93 | 'session_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}), 94 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}) 95 | } 96 | } 97 | 98 | complete_apps = ['docker_tutorial'] -------------------------------------------------------------------------------- /docker_tutorial/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhrp/docker-tutorial/6edf3d18e6a71a22fbd7bd1a0f371fee924d615a/docker_tutorial/migrations/__init__.py -------------------------------------------------------------------------------- /docker_tutorial/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from datetime import datetime 3 | 4 | __author__ = 'thatcher' 5 | 6 | 7 | class TutorialUser(models.Model): 8 | session_key = models.CharField(max_length=40, unique=True) # natural key 9 | timestamp = models.DateTimeField(auto_now=True, default=datetime.now) 10 | label = models.CharField(max_length=80, default='', blank=True) 11 | http_user_agent = models.TextField(default='', blank=True) 12 | http_remote_address = models.TextField(default='', blank=True) 13 | http_real_remote_address = models.TextField(default='', blank=True) 14 | http_accept_language = models.TextField(default='', blank=True) 15 | http_referrer = models.TextField(default='', blank=True) 16 | 17 | def __unicode__(self): 18 | return u"%s" % (self.id) 19 | 20 | 21 | class TutorialEvent(models.Model): 22 | 23 | NONE = 'none' 24 | START = 'start' 25 | COMMAND = 'command' 26 | NEXT = 'next' 27 | FEEDBACK = 'feedback' 28 | PEEK = 'peek' 29 | COMPLETE = 'complete' 30 | 31 | 32 | EVENT_TYPES = ( 33 | (NONE, u'No type set (debug)'), 34 | (START, u'Start tutorial'), 35 | (COMMAND, u'Command given'), 36 | (NEXT, u'Next Question'), 37 | (FEEDBACK, u'Feedback'), 38 | (PEEK, u'Peek'), 39 | (COMPLETE, u'Tutorial Complete') 40 | ) 41 | 42 | user = models.ForeignKey(TutorialUser) 43 | type = models.CharField(max_length=15, choices=EVENT_TYPES, blank=False) 44 | timestamp = models.DateTimeField(auto_now=True) 45 | question = models.IntegerField(null=True, blank=True) 46 | command = models.CharField(max_length=80, blank=True, default="") 47 | feedback = models.CharField(max_length=2000, blank=True, default="") 48 | 49 | @property 50 | def time_elapsed(self): 51 | time_elapsed = self.timestamp - TutorialEvent.objects.filter(user=self.user).order_by('pk')[0].timestamp 52 | return time_elapsed.seconds 53 | 54 | def __unicode__(self): 55 | return str(self.id) 56 | 57 | 58 | class DockerfileEvent(models.Model): 59 | 60 | user = models.ForeignKey(TutorialUser) 61 | item = models.CharField(max_length=15, blank=False) 62 | timestamp = models.DateTimeField(auto_now=True) 63 | level = models.IntegerField(null=True, blank=True) 64 | errors = models.IntegerField(null=True, blank=True) 65 | 66 | @property 67 | def time_elapsed(self): 68 | time_elapsed = self.timestamp - TutorialEvent.objects.filter(user=self.user).order_by('pk')[0].timestamp 69 | return time_elapsed.seconds 70 | 71 | def __unicode__(self): 72 | return str(self.id) 73 | 74 | 75 | class Subscriber(models.Model): 76 | 77 | email = models.EmailField(max_length=80) 78 | user = models.ForeignKey(TutorialUser) 79 | timestamp = models.DateTimeField(auto_now=True) 80 | from_level = models.IntegerField() 81 | 82 | class Meta: 83 | unique_together = ("email", "from_level") 84 | 85 | def __unicode__(self): 86 | return u"{}".format(self.email) 87 | -------------------------------------------------------------------------------- /docker_tutorial/static/css/tutorial-style.css: -------------------------------------------------------------------------------- 1 | .debug { 2 | border: 2px red dotted; 3 | background: #361919; 4 | /* Fall-back for browsers that don't support rgba */ 5 | 6 | background: rgba(54, 25, 25, 0.5); 7 | box-sizing: border-box; 8 | } 9 | /* 10 | Styles largely duplicates from the docker website 11 | */ 12 | .tutorial { 13 | font-family: "Cabin", "Helvetica Neue", Helvetica, Arial, sans-serif; 14 | font-size: 14px; 15 | line-height: 20px; 16 | color: #333333; 17 | } 18 | .tutorial h1, 19 | .tutorial h2, 20 | .tutorial h3, 21 | .tutorial h4, 22 | .tutorial h5, 23 | .tutorial h6 { 24 | margin: 10px 0; 25 | font-family: inherit; 26 | font-weight: normal; 27 | line-height: 20px; 28 | color: inherit; 29 | text-rendering: optimizelegibility; 30 | } 31 | .tutorial h1, 32 | .tutorial h2, 33 | .tutorial h3 { 34 | line-height: 40px; 35 | } 36 | .tutorial img { 37 | max-width: 100%; 38 | width: auto\9; 39 | height: auto; 40 | vertical-align: middle; 41 | border: 0; 42 | -ms-interpolation-mode: bicubic; 43 | } 44 | .tutorial select, 45 | .tutorial textarea, 46 | .tutorial input[type="text"], 47 | .tutorial input[type="password"], 48 | .tutorial input[type="datetime"], 49 | .tutorial input[type="datetime-local"], 50 | .tutorial input[type="date"], 51 | .tutorial input[type="month"], 52 | .tutorial input[type="time"], 53 | .tutorial input[type="week"], 54 | .tutorial input[type="number"], 55 | .tutorial input[type="email"], 56 | .tutorial input[type="url"], 57 | .tutorial input[type="search"], 58 | .tutorial input[type="tel"], 59 | .tutorial input[type="color"], 60 | .tutorial .uneditable-input { 61 | display: inline-block; 62 | height: 20px; 63 | padding: 4px 6px; 64 | margin-bottom: 10px; 65 | font-size: 14px; 66 | line-height: 20px; 67 | color: #555555; 68 | -webkit-border-radius: 4px; 69 | -moz-border-radius: 4px; 70 | border-radius: 4px; 71 | vertical-align: middle; 72 | } 73 | .tutorial button, 74 | .tutorial input, 75 | .tutorial select, 76 | .tutorial textarea { 77 | margin: 0; 78 | font-size: 100%; 79 | vertical-align: middle; 80 | } 81 | .tutorial label, 82 | .tutorial input, 83 | .tutorial button, 84 | .tutorial select, 85 | .tutorial textarea { 86 | font-size: 14px; 87 | font-weight: normal; 88 | line-height: 20px; 89 | } 90 | .tutorial code, 91 | .tutorial pre { 92 | padding: 0 3px 2px; 93 | font-family: Monaco, Menlo, Consolas, "Courier New", monospace; 94 | font-size: 12px; 95 | color: #333333; 96 | -webkit-border-radius: 3px; 97 | -moz-border-radius: 3px; 98 | border-radius: 3px; 99 | } 100 | .tutorial code { 101 | padding: 2px 4px; 102 | color: #d14; 103 | background-color: #f7f7f9; 104 | border: 1px solid #e1e1e8; 105 | white-space: nowrap; 106 | } 107 | /* 108 | New (non dupe) styles 109 | */ 110 | .tutorial p { 111 | margin-top: 0px; 112 | margin-bottom: 5px; 113 | line-height: 1.3em; 114 | } 115 | .tutorial h1, 116 | .tutorial h2, 117 | .tutorial h3, 118 | .tutorial h4 { 119 | margin-top: 5px; 120 | margin-bottom: 7px; 121 | color: #394D54; 122 | } 123 | .tutorial h3 { 124 | font-size: 24px; 125 | } 126 | .tutorial h4 { 127 | color: #008bb8; 128 | } 129 | #command .box { 130 | border: 1px grey solid; 131 | border-radius: 5px; 132 | padding: 15px; 133 | box-sizing: border-box; 134 | color: #a9a9a9; 135 | font-style: italic; 136 | } 137 | #commandShownText, 138 | #tipShownText { 139 | color: black !important; 140 | font-style: normal !important; 141 | } 142 | #command .hidden, 143 | #tips .hidden { 144 | display: none; 145 | visibility: hidden; 146 | height: 1px; 147 | width: 1px; 148 | } 149 | #ajax { 150 | position: absolute; 151 | bottom: 0; 152 | margin: 20px; 153 | } 154 | #resulttext { 155 | border-radius: 5px; 156 | box-sizing: border-box; 157 | color: #106e34; 158 | font-weight: bold; 159 | } 160 | #feedback { 161 | font-size: 0.4em; 162 | border: 1px #444444 solid; 163 | background-color: black; 164 | position: absolute; 165 | right: 0; 166 | bottom: 0px; 167 | margin-bottom: 5px; 168 | } 169 | #feedback input { 170 | margin: 5px; 171 | border-radius: 0px; 172 | position: relative; 173 | width: 400px; 174 | background-color: #dcdcdc; 175 | } 176 | #feedback button { 177 | margin: 5px 5px 5px 0; 178 | height: 30px; 179 | } 180 | #actions button { 181 | width: 45%; 182 | } 183 | .hidden { 184 | display: none; 185 | visibility: hidden; 186 | height: 1px; 187 | width: 1px; 188 | } 189 | /* styles for when it is not fullsize */ 190 | #overlay { 191 | position: relative; 192 | width: 100%; 193 | } 194 | #overlay.startsize { 195 | border: 3px solid #7fafc1; 196 | height: 300px; 197 | padding: 15px; 198 | } 199 | #overlay.fullsize { 200 | position: absolute; 201 | top: 0; 202 | right: 0; 203 | bottom: 0; 204 | left: 0; 205 | width: auto; 206 | height: auto; 207 | background-color: white; 208 | z-index: 1982; 209 | } 210 | #tutorialTop { 211 | display: none; 212 | position: absolute; 213 | height: 70px; 214 | left: 0; 215 | right: 0; 216 | width: 100%; 217 | background-color: #394D54; 218 | } 219 | #tutorialTop img { 220 | margin-bottom: 20px; 221 | display: inline-block; 222 | } 223 | #tutorialTop h1, 224 | #tutorialTop h2 { 225 | display: inline-block; 226 | color: white; 227 | margin-top: 0px; 228 | margin-bottom: 0px; 229 | } 230 | #tutorialTop h1 { 231 | font-size: 22px; 232 | color: white; 233 | font-weight: normal; 234 | margin-top: 20px; 235 | } 236 | #main { 237 | position: absolute; 238 | top: 0; 239 | right: 0; 240 | bottom: 0; 241 | left: 0; 242 | } 243 | #leftside.fullsize { 244 | box-sizing: border-box; 245 | width: 35%; 246 | position: absolute; 247 | display: block; 248 | top: 0; 249 | right: 0; 250 | bottom: 0; 251 | left: 0; 252 | margin-top: 70px; 253 | overflow-y: scroll; 254 | } 255 | #rightside { 256 | margin-top: 70px; 257 | } 258 | #instructions, 259 | #results, 260 | #actions, 261 | #tips, 262 | #command { 263 | padding: 5px 10px 0 10px; 264 | } 265 | #instructions .text, 266 | #results .text, 267 | #actions .text, 268 | #tips .text, 269 | #command .text, 270 | #instructions .assignment, 271 | #results .assignment, 272 | #actions .assignment, 273 | #tips .assignment, 274 | #command .assignment, 275 | #instructions .tiptext, 276 | #results .tiptext, 277 | #actions .tiptext, 278 | #tips .tiptext, 279 | #command .tiptext { 280 | margin-left: 60px; 281 | } 282 | #instructions .text, 283 | #results .text, 284 | #actions .text, 285 | #tips .text, 286 | #command .text { 287 | font-size: 16px; 288 | } 289 | #instructions circle { 290 | fill: #FF8100; 291 | } 292 | #results { 293 | z-index: 20; 294 | background-color: #e7e7e7; 295 | width: 400px; 296 | padding: 10px; 297 | position: absolute; 298 | box-sizing: border-box; 299 | right: 0; 300 | top: 0; 301 | margin-top: 100px; 302 | } 303 | #results circle { 304 | fill: green; 305 | } 306 | #results.intermediate circle { 307 | fill: #008bb8; 308 | } 309 | #results button { 310 | position: relative; 311 | float: right; 312 | margin-top: 10px; 313 | } 314 | #instructions .assignment { 315 | margin-top: 20px; 316 | border: 2px grey solid; 317 | padding: 15px; 318 | padding-top: 5px; 319 | } 320 | .circle { 321 | position: relative; 322 | float: left; 323 | font-size: 18px; 324 | margin-top: 15px; 325 | } 326 | #terminal { 327 | box-sizing: border-box; 328 | position: absolute; 329 | display: block; 330 | top: 0; 331 | right: 0; 332 | bottom: 0; 333 | left: 35%; 334 | } 335 | #terminal.smallsize { 336 | margin: 10px; 337 | margin-bottom: 65px; 338 | } 339 | #terminal.fullsize { 340 | margin: 0; 341 | margin-right: 15px; 342 | margin-top: 70px; 343 | padding-bottom: 0px; 344 | margin-bottom: 65px; 345 | } 346 | #terminal-extenstion { 347 | box-sizing: border-box; 348 | position: absolute; 349 | display: block; 350 | top: 0; 351 | right: 0; 352 | bottom: 0; 353 | left: 35%; 354 | background-color: black; 355 | margin-bottom: 0px; 356 | } 357 | #terminal-extenstion.smallsize { 358 | margin: 10px; 359 | } 360 | #terminal-extenstion.fullsize { 361 | margin: 0; 362 | margin-top: 70px; 363 | } 364 | #terminal a { 365 | color: #AAA; 366 | text-decoration: none; 367 | font-weight: normal; 368 | } 369 | #starttext { 370 | width: 200px; 371 | margin: 10px; 372 | } 373 | .hide-when-small { 374 | display: none; 375 | } 376 | .docker-btn { 377 | border: none; 378 | color: white; 379 | } 380 | .docker-btn-large { 381 | font-size: 22px; 382 | padding: 10px 15px 10px 15px; 383 | } 384 | .docker-btn-blue { 385 | border: none; 386 | color: white; 387 | background: #008bb8; 388 | /* Old browsers */ 389 | 390 | background: -moz-linear-gradient(top, #008bb8 0%, #00617f 100%); 391 | /* FF3.6+ */ 392 | 393 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #008bb8), color-stop(100%, #00617f)); 394 | /* Chrome,Safari4+ */ 395 | 396 | background: -webkit-linear-gradient(top, #008bb8 0%, #00617f 100%); 397 | /* Chrome10+,Safari5.1+ */ 398 | 399 | background: -o-linear-gradient(top, #008bb8 0%, #00617f 100%); 400 | /* Opera 11.10+ */ 401 | 402 | background: -ms-linear-gradient(top, #008bb8 0%, #00617f 100%); 403 | /* IE10+ */ 404 | 405 | background: linear-gradient(to bottom, #008bb8 0%, #00617f 100%); 406 | /* W3C */ 407 | 408 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#008bb8', endColorstr='#00617f', GradientType=0); 409 | /* IE6-9 */ 410 | 411 | } 412 | .docker-btn-blue:enabled:active { 413 | border: none; 414 | color: white; 415 | background: #00617f; 416 | /* Old browsers */ 417 | 418 | background: -moz-linear-gradient(top, #00617f 0%, #008bb8 100%); 419 | /* FF3.6+ */ 420 | 421 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #00617f), color-stop(100%, #008bb8)); 422 | /* Chrome,Safari4+ */ 423 | 424 | background: -webkit-linear-gradient(top, #00617f 0%, #008bb8 100%); 425 | /* Chrome10+,Safari5.1+ */ 426 | 427 | background: -o-linear-gradient(top, #00617f 0%, #008bb8 100%); 428 | /* Opera 11.10+ */ 429 | 430 | background: -ms-linear-gradient(top, #00617f 0%, #008bb8 100%); 431 | /* IE10+ */ 432 | 433 | background: linear-gradient(to bottom, #00617f 0%, #008bb8 100%); 434 | /* W3C */ 435 | 436 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00617f', endColorstr='#008bb8', GradientType=0); 437 | /* IE6-9 */ 438 | 439 | } 440 | .docker-btn-blue:disabled { 441 | color: white; 442 | background: #4f727c; 443 | /* Old browsers */ 444 | 445 | background: -moz-linear-gradient(top, #4f727c 0%, #89acb7 100%); 446 | /* FF3.6+ */ 447 | 448 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #4f727c), color-stop(100%, #89acb7)); 449 | /* Chrome,Safari4+ */ 450 | 451 | background: -webkit-linear-gradient(top, #4f727c 0%, #89acb7 100%); 452 | /* Chrome10+,Safari5.1+ */ 453 | 454 | background: -o-linear-gradient(top, #4f727c 0%, #89acb7 100%); 455 | /* Opera 11.10+ */ 456 | 457 | background: -ms-linear-gradient(top, #4f727c 0%, #89acb7 100%); 458 | /* IE10+ */ 459 | 460 | background: linear-gradient(to bottom, #4f727c 0%, #89acb7 100%); 461 | /* W3C */ 462 | 463 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#4f727c', endColorstr='#89acb7', GradientType=0); 464 | /* IE6-9 */ 465 | 466 | } 467 | #tutorialTop .closeButton { 468 | position: relative; 469 | float: right; 470 | margin: 6px; 471 | width: 18px; 472 | height: 18px; 473 | } 474 | #progress-indicator { 475 | margin-top: 20px; 476 | margin-right: 20px; 477 | float: right; 478 | cursor: hand; 479 | } 480 | .progress-marker { 481 | position: relative; 482 | float: left; 483 | margin-right: 5px; 484 | cursor: hand; 485 | } 486 | .progress-marker svg { 487 | width: 30px; 488 | height: 30px; 489 | cursor: pointer; 490 | } 491 | .progress-marker circle { 492 | fill: #eaeaea; 493 | } 494 | .progress-marker text { 495 | fill: black; 496 | } 497 | .progress-marker.complete circle { 498 | fill: green; 499 | } 500 | .progress-marker:hover circle { 501 | fill: #01b0ee; 502 | } 503 | .progress-marker:active circle { 504 | fill: #ff810a; 505 | } 506 | .progress-marker.active circle { 507 | fill: #ff810a; 508 | } 509 | .complete img { 510 | height: 45px; 511 | width: 45px; 512 | margin-top: 10px; 513 | margin-right: 10px; 514 | } 515 | .complete ol li { 516 | margin-bottom: 10px; 517 | font-weight: bold; 518 | } 519 | -------------------------------------------------------------------------------- /docker_tutorial/static/css/tutorial-style.less: -------------------------------------------------------------------------------- 1 | @informationblue: #008BB8; 2 | @tutorialTopHeight: 60px; 3 | @greySuperLight: #E7E7E7; 4 | @topBarHeight: 70px; 5 | 6 | 7 | .debug { 8 | border: 2px red dotted; 9 | background: rgb(54, 25, 25); /* Fall-back for browsers that don't support rgba */ 10 | background: rgba(54, 25, 25, .5); 11 | box-sizing: border-box; 12 | } 13 | 14 | /* 15 | Styles largely duplicates from the docker website 16 | */ 17 | 18 | .tutorial { 19 | font-family: "Cabin", "Helvetica Neue", Helvetica, Arial, sans-serif; 20 | font-size: 14px; 21 | line-height: 20px; 22 | color: #333333; 23 | 24 | h1, h2, h3, h4, h5, h6 { 25 | margin: 10px 0; 26 | font-family: inherit; 27 | font-weight: normal; 28 | line-height: 20px; 29 | color: inherit; 30 | text-rendering: optimizelegibility; 31 | } 32 | 33 | h1, h2, h3 { 34 | line-height: 40px; 35 | } 36 | 37 | img { 38 | max-width: 100%; 39 | width: auto\9; 40 | height: auto; 41 | vertical-align: middle; 42 | border: 0; 43 | -ms-interpolation-mode: bicubic; 44 | } 45 | 46 | select, textarea, input[type="text"], input[type="password"], input[type="datetime"], input[type="datetime-local"], input[type="date"], input[type="month"], input[type="time"], input[type="week"], input[type="number"], input[type="email"], input[type="url"], input[type="search"], input[type="tel"], input[type="color"], .uneditable-input { 47 | display: inline-block; 48 | height: 20px; 49 | padding: 4px 6px; 50 | margin-bottom: 10px; 51 | font-size: 14px; 52 | line-height: 20px; 53 | color: #555555; 54 | -webkit-border-radius: 4px; 55 | -moz-border-radius: 4px; 56 | border-radius: 4px; 57 | vertical-align: middle; 58 | } 59 | 60 | button, input, select, textarea { 61 | margin: 0; 62 | font-size: 100%; 63 | vertical-align: middle; 64 | } 65 | 66 | label, input, button, select, textarea { 67 | font-size: 14px; 68 | font-weight: normal; 69 | line-height: 20px; 70 | } 71 | 72 | code, pre { 73 | padding: 0 3px 2px; 74 | font-family: Monaco, Menlo, Consolas, "Courier New", monospace; 75 | font-size: 12px; 76 | color: #333333; 77 | -webkit-border-radius: 3px; 78 | -moz-border-radius: 3px; 79 | border-radius: 3px; 80 | } 81 | 82 | code { 83 | padding: 2px 4px; 84 | color: #d14; 85 | background-color: #f7f7f9; 86 | border: 1px solid #e1e1e8; 87 | white-space: nowrap; 88 | } 89 | 90 | } 91 | 92 | /* 93 | New (non dupe) styles 94 | */ 95 | 96 | 97 | .tutorial { 98 | p { 99 | margin-top: 0px; 100 | margin-bottom: 5px; 101 | line-height: 1.3em; 102 | } 103 | 104 | h1, h2, h3, h4{ 105 | margin-top: 5px; 106 | margin-bottom: 7px; 107 | color: #394D54; 108 | } 109 | 110 | h3 { 111 | font-size: 24px; 112 | } 113 | 114 | h4 { 115 | color: @informationblue; 116 | // margin-bottom: 5px; 117 | } 118 | } 119 | 120 | 121 | #command .box{ 122 | // .debug; 123 | border: 1px grey solid; 124 | border-radius: 5px; 125 | padding: 15px; 126 | box-sizing: border-box; 127 | color: #a9a9a9; 128 | font-style: italic; 129 | // style="width: 100%; height: 100%" 130 | } 131 | 132 | #commandShownText, #tipShownText { 133 | color: black !important; 134 | font-style: normal !important; 135 | } 136 | 137 | #command .hidden, #tips .hidden { 138 | display: none; 139 | visibility: hidden; 140 | height: 1px; width: 1px; 141 | } 142 | 143 | #ajax { 144 | position: absolute; 145 | bottom: 0; 146 | margin: 20px; 147 | // border: 1px red dotted; 148 | } 149 | 150 | #resulttext { 151 | border-radius: 5px; 152 | box-sizing: border-box; 153 | color: #106e34; 154 | font-weight: bold; 155 | } 156 | 157 | 158 | 159 | #feedback { 160 | font-size: 0.4em; 161 | border: 1px #444444 solid; 162 | background-color: black; 163 | position: absolute; 164 | right: 0; 165 | bottom: 0px; 166 | margin-bottom: 5px; 167 | 168 | input { 169 | margin: 5px; 170 | border-radius: 0px; 171 | position: relative; 172 | width: 400px; 173 | background-color: #dcdcdc; 174 | } 175 | button { 176 | margin: 5px 5px 5px 0; 177 | height: 30px; 178 | } 179 | } 180 | 181 | 182 | #actions button { 183 | width: 45%; 184 | } 185 | 186 | .hidden { 187 | display: none; 188 | visibility: hidden; 189 | height: 1px; 190 | width: 1px; 191 | } 192 | 193 | 194 | /* styles for when it is not fullsize */ 195 | 196 | 197 | //#overlay .hide-when-full { 198 | // display: none; 199 | //} 200 | 201 | 202 | #overlay { 203 | position: relative; 204 | width: 100%; 205 | 206 | &.startsize { 207 | border: 3px solid #7fafc1; 208 | height: 300px; 209 | padding: 15px; 210 | } 211 | 212 | &.fullsize { 213 | position: absolute; 214 | top: 0; 215 | right: 0; 216 | bottom: 0; 217 | left: 0; 218 | width: auto; 219 | height: auto; 220 | 221 | // here we set a background color for the overlay, so the elements below don't shine through 222 | background-color: white; 223 | z-index: 1982; 224 | } 225 | } 226 | 227 | 228 | #tutorialTop { 229 | 230 | display: none; 231 | 232 | position: absolute; 233 | height: @topBarHeight; 234 | left: 0; 235 | right: 0; 236 | width: 100%; 237 | background-color: #394D54; 238 | 239 | img { 240 | margin-bottom: 20px; 241 | display: inline-block; 242 | } 243 | h1, h2 { 244 | display: inline-block; 245 | color: white; 246 | margin-top: 0px; 247 | margin-bottom: 0px; 248 | } 249 | 250 | h1 { 251 | font-size: 22px; 252 | // font-size: 2.2rem; 253 | color: white; 254 | font-weight: normal; 255 | margin-top: 20px; 256 | } 257 | } 258 | 259 | 260 | 261 | #main { 262 | position: absolute; 263 | top: 0; 264 | right: 0; 265 | bottom: 0; 266 | left: 0; 267 | 268 | &.fullsize { 269 | // margin-top: 0px; 270 | // top: @topBarHeight; 271 | } 272 | 273 | } 274 | 275 | #leftside { 276 | &.fullsize { 277 | box-sizing: border-box; 278 | width: 35%; 279 | position: absolute; 280 | display: block; 281 | top: 0; 282 | right: 0; 283 | bottom: 0; 284 | left: 0; 285 | 286 | margin-top: @topBarHeight; 287 | overflow-y: scroll; 288 | // .debug; 289 | } 290 | 291 | } 292 | 293 | #rightside { 294 | margin-top: @topBarHeight; 295 | } 296 | 297 | 298 | #instructions, #results, #actions, #tips, #command { 299 | padding: 5px 10px 0 10px; 300 | .text, .assignment, .tiptext { 301 | margin-left: 60px; 302 | } 303 | .text { 304 | font-size: 16px; 305 | } 306 | } 307 | 308 | #instructions { 309 | circle { fill: #FF8100; } 310 | } 311 | 312 | #results { 313 | position: absolute; 314 | z-index: 20; 315 | background-color: @greySuperLight; 316 | width: 400px; 317 | // min-height: 130px; 318 | padding: 10px; 319 | position:absolute; 320 | box-sizing: border-box; 321 | right:0; 322 | top: 0; 323 | margin-top: @topBarHeight + 30px; 324 | 325 | circle { fill: green; } 326 | 327 | &.intermediate { 328 | circle { fill: @informationblue } 329 | } 330 | 331 | button { 332 | position: relative; 333 | float: right; 334 | margin-top: 10px; 335 | // margin-bottom: 15px; 336 | } 337 | } 338 | 339 | #instructions { 340 | // .debug; 341 | .assignment { 342 | margin-top: 20px; 343 | border: 2px grey solid; 344 | padding: 15px; 345 | padding-top: 5px; 346 | } 347 | } 348 | 349 | .circle { 350 | position: relative; 351 | float: left; 352 | font-size: 18px; 353 | margin-top: 15px; 354 | } 355 | 356 | #terminal { 357 | box-sizing: border-box; 358 | position: absolute; 359 | display: block; 360 | top: 0; 361 | right: 0; 362 | bottom: 0; 363 | left: 35%; 364 | 365 | &.smallsize { 366 | margin: 10px; 367 | // padding-bottom: 10px; 368 | margin-bottom: 65px; 369 | } 370 | 371 | &.fullsize { 372 | margin: 0; 373 | margin-right: 15px; 374 | margin-top: @topBarHeight; 375 | padding-bottom: 0px; 376 | margin-bottom: 65px; 377 | } 378 | } 379 | 380 | #terminal-extenstion { 381 | box-sizing: border-box; 382 | position: absolute; 383 | display: block; 384 | top: 0; 385 | right: 0; 386 | bottom: 0; 387 | left: 35%; 388 | 389 | background-color: black; 390 | margin-bottom: 0px; 391 | 392 | &.smallsize { 393 | margin: 10px; 394 | } 395 | 396 | &.fullsize { 397 | margin: 0; 398 | margin-top: @topBarHeight; 399 | } 400 | 401 | } 402 | 403 | #terminal a { 404 | color: #AAA; 405 | text-decoration: none; 406 | font-weight: normal; 407 | } 408 | 409 | 410 | #starttext { 411 | width: 200px; 412 | margin: 10px; 413 | } 414 | 415 | .hide-when-small { 416 | display: none; 417 | } 418 | 419 | .hide-when-full { 420 | // display: none; 421 | } 422 | 423 | .docker-btn { 424 | border: none; 425 | color: white; 426 | 427 | } 428 | 429 | .docker-btn-large { 430 | font-size: 22px; 431 | padding: 10px 15px 10px 15px; 432 | } 433 | 434 | .docker-btn-blue { 435 | 436 | .docker-btn; 437 | 438 | background: #008bb8; /* Old browsers */ 439 | background: -moz-linear-gradient(top, #008bb8 0%, #00617f 100%); /* FF3.6+ */ 440 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#008bb8), color-stop(100%,#00617f)); /* Chrome,Safari4+ */ 441 | background: -webkit-linear-gradient(top, #008bb8 0%,#00617f 100%); /* Chrome10+,Safari5.1+ */ 442 | background: -o-linear-gradient(top, #008bb8 0%,#00617f 100%); /* Opera 11.10+ */ 443 | background: -ms-linear-gradient(top, #008bb8 0%,#00617f 100%); /* IE10+ */ 444 | background: linear-gradient(to bottom, #008bb8 0%,#00617f 100%); /* W3C */ 445 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#008bb8', endColorstr='#00617f',GradientType=0 ); /* IE6-9 */ 446 | } 447 | 448 | .docker-btn-blue:enabled:active { 449 | 450 | .docker-btn; 451 | 452 | background: #00617f; /* Old browsers */ 453 | background: -moz-linear-gradient(top, #00617f 0%, #008bb8 100%); /* FF3.6+ */ 454 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#00617f), color-stop(100%,#008bb8)); /* Chrome,Safari4+ */ 455 | background: -webkit-linear-gradient(top, #00617f 0%,#008bb8 100%); /* Chrome10+,Safari5.1+ */ 456 | background: -o-linear-gradient(top, #00617f 0%,#008bb8 100%); /* Opera 11.10+ */ 457 | background: -ms-linear-gradient(top, #00617f 0%,#008bb8 100%); /* IE10+ */ 458 | background: linear-gradient(to bottom, #00617f 0%,#008bb8 100%); /* W3C */ 459 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00617f', endColorstr='#008bb8',GradientType=0 ); /* IE6-9 */ 460 | 461 | } 462 | 463 | .docker-btn-blue:disabled { 464 | color: white; 465 | 466 | background: #4f727c; /* Old browsers */ 467 | background: -moz-linear-gradient(top, #4f727c 0%, #89acb7 100%); /* FF3.6+ */ 468 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4f727c), color-stop(100%,#89acb7)); /* Chrome,Safari4+ */ 469 | background: -webkit-linear-gradient(top, #4f727c 0%,#89acb7 100%); /* Chrome10+,Safari5.1+ */ 470 | background: -o-linear-gradient(top, #4f727c 0%,#89acb7 100%); /* Opera 11.10+ */ 471 | background: -ms-linear-gradient(top, #4f727c 0%,#89acb7 100%); /* IE10+ */ 472 | background: linear-gradient(to bottom, #4f727c 0%,#89acb7 100%); /* W3C */ 473 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#4f727c', endColorstr='#89acb7',GradientType=0 ); /* IE6-9 */ 474 | } 475 | 476 | #tutorialTop .closeButton { 477 | position: relative; 478 | float: right; 479 | margin: 6px; 480 | width: 18px; 481 | height: 18px; 482 | } 483 | 484 | #progress-indicator { 485 | margin-top: 20px; 486 | margin-right: 20px; 487 | float: right; 488 | // .debug; 489 | // width: 100px; 490 | // height: 100px; 491 | cursor: hand; 492 | } 493 | 494 | .progress-marker { 495 | 496 | position:relative; 497 | float: left; 498 | margin-right: 5px; 499 | cursor: hand; 500 | // top:5; 501 | // left:5; 502 | // z-index:1; 503 | // .debug; 504 | svg { 505 | width:30px; 506 | height:30px; 507 | cursor: pointer; 508 | } 509 | 510 | circle { fill: #eaeaea; } 511 | text { fill: black; } 512 | 513 | &.complete { 514 | circle { fill: green; } 515 | } 516 | 517 | &:hover { 518 | circle { fill: #01b0ee; } 519 | } 520 | 521 | &:active { 522 | circle { fill: #ff810a; } 523 | } 524 | 525 | &.active { 526 | circle { fill: #ff810a; } 527 | } 528 | } 529 | 530 | .complete { 531 | img { 532 | height: 45px; 533 | width: 45px; 534 | margin-top: 10px; 535 | margin-right: 10px; 536 | } 537 | 538 | ol { 539 | li { 540 | margin-bottom: 10px; 541 | font-weight: bold; 542 | } 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /docker_tutorial/static/img/circle-green.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 1 6 | -------------------------------------------------------------------------------- /docker_tutorial/static/img/docker-letters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhrp/docker-tutorial/6edf3d18e6a71a22fbd7bd1a0f371fee924d615a/docker_tutorial/static/img/docker-letters.png -------------------------------------------------------------------------------- /docker_tutorial/static/img/docker-topicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhrp/docker-tutorial/6edf3d18e6a71a22fbd7bd1a0f371fee924d615a/docker_tutorial/static/img/docker-topicon.png -------------------------------------------------------------------------------- /docker_tutorial/static/img/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhrp/docker-tutorial/6edf3d18e6a71a22fbd7bd1a0f371fee924d615a/docker_tutorial/static/img/email.png -------------------------------------------------------------------------------- /docker_tutorial/static/img/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhrp/docker-tutorial/6edf3d18e6a71a22fbd7bd1a0f371fee924d615a/docker_tutorial/static/img/facebook.png -------------------------------------------------------------------------------- /docker_tutorial/static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhrp/docker-tutorial/6edf3d18e6a71a22fbd7bd1a0f371fee924d615a/docker_tutorial/static/img/favicon.png -------------------------------------------------------------------------------- /docker_tutorial/static/img/fullscreen_exit_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhrp/docker-tutorial/6edf3d18e6a71a22fbd7bd1a0f371fee924d615a/docker_tutorial/static/img/fullscreen_exit_32x32.png -------------------------------------------------------------------------------- /docker_tutorial/static/img/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhrp/docker-tutorial/6edf3d18e6a71a22fbd7bd1a0f371fee924d615a/docker_tutorial/static/img/twitter.png -------------------------------------------------------------------------------- /docker_tutorial/static/js/steps.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | This is the main script file. It can attach to the terminal 3 | ### 4 | 5 | COMPLETE_URL = "/whats-next/" 6 | 7 | 8 | ### 9 | Array of question objects 10 | ### 11 | 12 | staticDockerPs = """ 13 | ID IMAGE COMMAND CREATED STATUS PORTS 14 | """ 15 | 16 | 17 | q = [] 18 | q.push ({ 19 | html: """ 20 |

Getting started

21 |

There are actually two programs: The Docker daemon, which is a server process and which manages all the 22 | containers, and the Docker client, which acts as a remote control on the daemon. On most systems, like in this 23 | emulator, both execute on the same host.

24 | """ 25 | assignment: """ 26 |

Assignment

27 |

Check which Docker versions are running

28 |

This will help you verify the daemon is running and you can connect to it. If you see which version is running 29 | you know you are all set.

30 | """ 31 | tip: "

Try typing docker to see the full list of accepted arguments

32 |

This emulator provides only a limited set of shell and Docker commands, so some commands may not work as expected

" 33 | command_expected: ['docker', 'version'] 34 | result: """

Well done! Let's move to the next assignment.

""" 35 | }) 36 | 37 | q.push ({ 38 | html: """ 39 |

Searching for images

40 |

The easiest way to get started is to use a container image from someone else. Container images are 41 | available on the Docker index, a central place to store images. You can find them online at 42 | index.docker.io 43 | and by using the commandline

44 | """ 45 | assignment: """ 46 |

Assignment

47 |

Use the commandline to search for an image called tutorial

48 | """ 49 | command_expected: ['docker', 'search', 'tutorial'] 50 | result: """

You found it!

""" 51 | tip: "the format is docker search <string>" 52 | }) 53 | 54 | q.push ({ 55 | html: """ 56 |

Downloading container images

57 |

Container images can be downloaded just as easily, using docker pull.

58 |

For images from the central index, the name you specify is constructed as <username>/<repository>

59 |

A group of special, trusted images such as the ubuntu base image can be retrieved by just their name <repository>.

60 | """ 61 | assignment: 62 | """ 63 |

Assignment

64 |

Please download the tutorial image you have just found

65 | """ 66 | command_expected: ['docker', 'pull', 'learn/tutorial'] 67 | result: """

Cool. Look at the results. You'll see that Docker has downloaded a number of layers. In Docker all images (except the base image) are made up of several cumulative layers.

""" 68 | tip: """

Don't forget to pull the full name of the repository e.g. 'learn/tutorial'

69 |

Look under 'show expected command if you're stuck.

70 | """ 71 | }) 72 | 73 | 74 | q.push ({ 75 | html: """ 76 |

Hello world from a container

77 |

You can think about containers as a process in a box. The box contains everything the process might need, so 78 | it has the filesystem, system libraries, shell and such, but by default none of it is started or run.

79 |

You 'start' a container by running a process in it. This process is the only process run, so when 80 | it completes the container is fully stopped. 81 | """ 82 | assignment: """ 83 |

Assignment

84 |

Make our freshly loaded container image output "hello world"

85 |

To do so you should run 'echo' in the container and have that say "hello world" 86 | 87 | """ 88 | command_expected: ["docker", "run", "learn/tutorial", "echo", "hello"] 89 | command_show: ["docker", "run", "learn/tutorial", 'echo "hello world"'] 90 | 91 | result: """

Great! Hellooooo World!

You have just started a container and executed a program inside of it, when 92 | the program stopped, so did the container.""" 93 | intermediateresults: [ 94 | () -> """

You seem to be almost there. Did you give the command `echo "hello world"` """, 95 | () -> """

You've got the arguments right. Did you get the command? Try /bin/bash ?

""" 96 | ] 97 | tip: """ 98 |

The command docker run takes a minimum of two arguments. An image name, and the command you want to execute 99 | within that image.

100 |

Check the expected command below if it does not work as expected

101 | """ 102 | }) 103 | 104 | q.push ({ 105 | html: """ 106 |

Installing things in the container

107 |

Next we are going to install a simple program (ping) in the container. The image is based upon ubuntu, so you 108 | can run the command apt-get install -y ping in the container.

109 |

Note that even though the container stops right after a command completes, the changes are not forgotten.

110 | """ 111 | assignment: """ 112 |

Assignment

113 |

Install 'ping' on top of the learn/tutorial image.

114 | """ 115 | command_expected: ["docker", "run", "learn/tutorial", "apt-get", "install", "-y", "ping"] 116 | result: """

That worked! You have installed a program on top of a base image. Your changes to the filesystem have been 117 | kept, but are not yet saved.

""" 118 | intermediateresults: [ 119 | () -> """

Not specifying -y on the apt-get install command will work for ping, because it has no other dependencies, but 120 | it will fail when apt-get wants to install dependencies. To get into the habit, please add -y after apt-get.

""", 121 | ] 122 | tip: """ 123 |

Don't forget to use -y for noninteractive mode installation

124 |

Not specifying -y on the apt-get install command will fail for most commands because it expects you to accept 125 | (y/n) but you cannot respond. 126 |

127 | """ 128 | }) 129 | 130 | q.push ({ 131 | html: """ 132 |

Save your changes

133 |

After you make changes (by running a command inside a container), you probably want to save those changes. 134 | This will enable you to later start from this point onwards.

135 |

With Docker, the process of saving the state is called committing. Commit basically saves the difference 136 | between the old image and the new state. The result is a new layer.

137 | """ 138 | assignment: """ 139 |

Assignment

140 |

First use docker ps -l to find the ID of the container you created by installing ping.

141 |

Then save (commit) this container with the repository name 'learn/ping'

142 | """ 143 | command_expected: ["docker", "commit", "698", "learn/ping"] 144 | command_show: ["docker", "commit", "698", 'learn/ping'] 145 | result: """

That worked! Please take note that Docker has returned a new ID. This id is the image id.

""" 146 | intermediateresults: [ () -> """You have not specified the correct repository name to commit to (learn/ping). This works, but giving your images a name 147 | makes them much easier to work with."""] 148 | tip: """""" 153 | }) 154 | 155 | 156 | q.push ({ 157 | html: """ 158 |

Run your new image

159 |

Now you have basically setup a complete, self contained environment with the 'ping' program installed.

160 |

Your image can now be run on any host that runs Docker.

161 |

Lets run this image on this machine.

162 | """ 163 | assignment: """ 164 |

Assignment

165 |

Run the ping program to ping www.google.com

166 | 167 | """ 168 | command_expected: ["docker", "run", 'learn/ping', 'ping', 'google.com' ] 169 | result: """

That worked! Note that normally you can use Ctrl-C to disconnect. The container will keep running. This 170 | container will disconnect automatically.

""" 171 | intermediateresults: [ () -> """You have not specified a repository name. This is not wrong, but giving your images a name 172 | make them much easier to work with."""] 173 | tip: """""" 176 | }) 177 | 178 | 179 | 180 | 181 | q.push ({ 182 | html: """ 183 |

Check your running image

184 |

You now have a running container. Let's see what is going on.

185 |

Using docker ps we can see a list of all running containers, and using docker inspect 186 | we can see all sorts of useful information about this container.

187 | """ 188 | assignment: """ 189 |

Assignment

190 |

Find the container id of the running container, and then inspect the container using docker inspect.

191 | 192 | """ 193 | command_expected: ["docker", "inspect", "efe" ] 194 | result: """

Success! Have a look at the output. You can see the ip-address, status and other information.

""" 195 | intermediateresults: [ () -> """You have not specified a repository name. This is not wrong, but giving your images a name 196 | make them much easier to work with."""] 197 | tip: """""" 200 | currentDockerPs: 201 | """ 202 | ID IMAGE COMMAND CREATED STATUS PORTS 203 | efefdc74a1d5 learn/ping:latest ping www.google.com 37 seconds ago Up 36 seconds 204 | """ 205 | 206 | }) 207 | 208 | 209 | 210 | q.push ({ 211 | html: """ 212 |

Push your image to the index

213 |

Now you have verified that your application container works, you can share it.

214 |

Remember you pulled (downloaded) the learn/tutorial image from the index? You can also share your built images 215 | to the index by pushing (uploading) them to there. That way you can easily retrieve them for re-use and share them 216 | with others.

217 | """ 218 | assignment: """ 219 |

Assignment

220 |

Push your container image learn/ping to the index

221 | 222 | """ 223 | #command_expected: ["docker", "push", "learn/ping"] 224 | command_expected: ["will_never_be_valid"] 225 | command_show: ["docker", "push", "learn/ping"] 226 | result: """""" 227 | intermediateresults: 228 | [ 229 | () -> 230 | $('#instructions .assignment').hide() 231 | $('#tips, #command').hide() 232 | 233 | $('#instructions .text').html(""" 234 |
235 |

Congratulations!

236 |

You have mastered the basic docker commands!

237 |

Did you enjoy this tutorial? Share it!

238 |

239 | 240 | 241 | 242 |

243 |

Your next steps

244 |
    245 |
  1. Register for news and updates on Docker (opens in new window)
  2. 246 |
  3. Follow us on twitter (opens in new window)
  4. 247 |
  5. Close this tutorial, and continue with the rest of the getting started.
  6. 248 |
249 |

- Or -

250 |

Continue to learn about the way to automatically build your containers from a file.

Start Dockerfile tutorial

251 | 252 |
253 | """) 254 | 255 | 256 | data = { type: EVENT_TYPES.complete } 257 | logEvent(data) 258 | 259 | return """

All done!. You are now pushing a container image to the index. You can see that push, just like pull, happens layer by layer.

""" 260 | ] 261 | tip: """""" 267 | finishedCallback: () -> 268 | webterm.clear() 269 | webterm.echo( myTerminal() ) 270 | 271 | 272 | }) 273 | 274 | 275 | # the index arr 276 | questions = [] 277 | 278 | 279 | 280 | 281 | ### 282 | Register the terminal 283 | ### 284 | 285 | @webterm = $('#terminal').terminal(interpreter, basesettings) 286 | 287 | 288 | 289 | 290 | EVENT_TYPES = 291 | none: "none" 292 | start: "start" 293 | command: "command" 294 | next: "next" 295 | peek: "peek" 296 | feedback: "feedback" 297 | complete: "complete" 298 | 299 | 300 | 301 | ### 302 | Sending events to the server 303 | ### 304 | 305 | logEvent = (data, feedback) -> 306 | ajax_load = "loading......"; 307 | loadUrl = "/tutorial/api/"; 308 | if not feedback 309 | callback = (responseText) -> $("#ajax").html(responseText) 310 | else 311 | callback = (responseText) -> 312 | results.set("Thank you for your feedback! We appreciate it!", true) 313 | $('#feedbackInput').val("") 314 | $("#ajax").html(responseText) 315 | 316 | if not data then data = {type: EVENT_TYPES.none} 317 | data.question = current_question 318 | 319 | 320 | $("#ajax").html(ajax_load); 321 | $.post(loadUrl, data, callback, "html") 322 | 323 | 324 | 325 | ### 326 | Event handlers 327 | ### 328 | 329 | 330 | ## next 331 | $('#buttonNext').click (e) -> 332 | 333 | # disable the button to prevent spacebar to hit it when typing in the terminal 334 | this.setAttribute('disabled','disabled') 335 | console.log(e) 336 | next() 337 | 338 | $('#buttonFinish').click -> 339 | window.open(COMPLETE_URL) 340 | 341 | ## previous 342 | $('#buttonPrevious').click -> 343 | previous() 344 | $('#results').hide() 345 | 346 | ## Stop mousewheel on left side, and manually move it. 347 | $('#leftside').bind('mousewheel', 348 | (event, delta, deltaX, deltaY) -> 349 | this.scrollTop += deltaY * -30 350 | event.preventDefault() 351 | ) 352 | 353 | ## submit feedback 354 | $('#feedbackSubmit').click -> 355 | feedback = $('#feedbackInput').val() 356 | data = { type: EVENT_TYPES.feedback, feedback: feedback} 357 | logEvent(data, feedback=true) 358 | 359 | ## fullsize 360 | $('#fullSizeOpen').click -> 361 | goFullScreen() 362 | 363 | @goFullScreen = () -> 364 | console.debug("going to fullsize mode") 365 | $('.togglesize').removeClass('startsize').addClass('fullsize') 366 | 367 | $('.hide-when-small').css({ display: 'inherit' }) 368 | $('.hide-when-full').css({ display: 'none' }) 369 | 370 | next(0) 371 | 372 | webterm.resize() 373 | 374 | # send the next event after a short timeout, so it doesn't come at the same time as the next() event 375 | # in the beginning. Othewise two sessions will appear to have been started. 376 | # This will make the order to appear wrong, but that's not much of an issue. 377 | 378 | setTimeout( () -> 379 | logEvent( { type: EVENT_TYPES.start } ) 380 | , 3000) 381 | 382 | 383 | ## leave fullsize 384 | $('#fullSizeClose').click -> 385 | leaveFullSizeMode() 386 | 387 | @leaveFullSizeMode = () -> 388 | console.debug "leaving full-size mode" 389 | 390 | $('.togglesize').removeClass('fullsize').addClass('startsize') 391 | 392 | $('.hide-when-small').css({ display: 'none' }) 393 | $('.hide-when-full').css({ display: 'inherit' }) 394 | 395 | webterm.resize() 396 | 397 | ## click on tips 398 | $('#command').click () -> 399 | if not $('#commandHiddenText').hasClass('hidden') 400 | $('#commandHiddenText').addClass("hidden").hide() 401 | $('#commandShownText').hide().removeClass("hidden").fadeIn() 402 | 403 | data = { type: EVENT_TYPES.peek } 404 | logEvent(data) 405 | 406 | 407 | 408 | ### 409 | Navigation amongst the questions 410 | ### 411 | 412 | 413 | current_question = 0 414 | next = (which) -> 415 | # before increment clear style from previous question progress indicator 416 | $('#marker-' + current_question).addClass("complete").removeClass("active") 417 | 418 | if not which and which != 0 419 | current_question++ 420 | else 421 | current_question = which 422 | 423 | questions[current_question]() 424 | results.clear() 425 | @webterm.focus() 426 | 427 | if not $('#commandShownText').hasClass('hidden') 428 | $('#commandShownText').addClass("hidden") 429 | $('#commandHiddenText').removeClass("hidden").show() 430 | 431 | # enable history navigation 432 | history.pushState({}, "", "#" + current_question); 433 | data = { 'type': EVENT_TYPES.next } 434 | logEvent(data) 435 | 436 | # change the progress indicator 437 | $('#marker-' + current_question).removeClass("complete").addClass("active") 438 | 439 | $('#question-number').find('text').get(0).textContent = current_question 440 | 441 | # show in the case they were hidden by the complete step. 442 | $('#instructions .assignment').show() 443 | $('#tips, #command').show() 444 | 445 | 446 | return 447 | 448 | previous = () -> 449 | current_question-- 450 | questions[current_question]() 451 | results.clear() 452 | @webterm.focus() 453 | return 454 | 455 | 456 | 457 | results = { 458 | set: (htmlText, intermediate) -> 459 | if intermediate 460 | console.debug "intermediate text received" 461 | $('#results').addClass('intermediate') 462 | $('#buttonNext').hide() 463 | else 464 | $('#buttonNext').show() 465 | 466 | window.setTimeout ( () -> 467 | $('#resulttext').html(htmlText) 468 | $('#results').fadeIn() 469 | $('#buttonNext').removeAttr('disabled') 470 | ), 300 471 | 472 | clear: -> 473 | $('#resulttext').html("") 474 | $('#results').fadeOut('slow') 475 | # $('#buttonNext').addAttr('disabled') 476 | } 477 | 478 | 479 | 480 | ### 481 | Transform question objects into functions 482 | ### 483 | 484 | buildfunction = (q) -> 485 | _q = q 486 | return -> 487 | console.debug("function called") 488 | 489 | $('#instructions').hide().fadeIn() 490 | $('#instructions .text').html(_q.html) 491 | $('#instructions .assignment').html(_q.assignment) 492 | $('#tipShownText').html(_q.tip) 493 | if _q.command_show 494 | $('#commandShownText').html(_q.command_show.join(' ')) 495 | else 496 | $('#commandShownText').html(_q.command_expected.join(' ')) 497 | 498 | if _q.currentDockerPs? 499 | window.currentDockerPs = _q.currentDockerPs 500 | else 501 | window.currentDockerPs = staticDockerPs 502 | 503 | if _q.finishedCallback? 504 | window.finishedCallback = q.finishedCallback 505 | else 506 | window.finishedCallback = () -> return "" 507 | 508 | window.immediateCallback = (input, stop) -> 509 | if stop == true # prevent the next event from happening 510 | doNotExecute = true 511 | else 512 | doNotExecute = false 513 | 514 | if doNotExecute != true 515 | console.log (input) 516 | 517 | data = { 'type': EVENT_TYPES.command, 'command': input.join(' '), 'result': 'fail' } 518 | 519 | # Was like this: if not input.switches.containsAllOfThese(_q.arguments) 520 | if input.containsAllOfTheseParts(_q.command_expected) 521 | data.result = 'success' 522 | 523 | setTimeout( ( -> 524 | @webterm.disable() 525 | $('#buttonNext').focus() 526 | ), 1000) 527 | 528 | results.set(_q.result) 529 | console.debug "contains match" 530 | else 531 | console.debug("wrong command received") 532 | 533 | # call function to submit data 534 | logEvent(data) 535 | return 536 | 537 | window.intermediateResults = (input) -> 538 | if _q.intermediateresults 539 | results.set(_q.intermediateresults[input](), intermediate=true) 540 | return 541 | 542 | 543 | statusMarker = $('#progress-marker-0') 544 | progressIndicator = $('#progress-indicator')# 545 | 546 | drawStatusMarker = (i) -> 547 | if i == 0 548 | marker = statusMarker 549 | else 550 | marker = statusMarker.clone() 551 | marker.appendTo(progressIndicator) 552 | 553 | marker.attr("id", "marker-" + i) 554 | marker.find('text').get(0).textContent = i 555 | marker.click( -> next(i) ) 556 | 557 | 558 | questionNumber = 0 559 | for question in q 560 | f = buildfunction(question) 561 | questions.push(f) 562 | drawStatusMarker(questionNumber) 563 | questionNumber++ 564 | 565 | 566 | ### 567 | Initialization of program 568 | ### 569 | 570 | #load the first question, or if the url hash is set, use that 571 | if (window.location.hash) 572 | try 573 | currentquestion = window.location.hash.split('#')[1].toNumber() 574 | # questions[currentquestion]() 575 | # current_question = currentquestion 576 | next(currentquestion) 577 | 578 | catch err 579 | questions[0]() 580 | else 581 | questions[0]() 582 | 583 | $('#results').hide() 584 | 585 | -------------------------------------------------------------------------------- /docker_tutorial/static/js/steps.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | /* 3 | This is the main script file. It can attach to the terminal 4 | */ 5 | 6 | 7 | (function() { 8 | var COMPLETE_URL, EVENT_TYPES, buildfunction, current_question, currentquestion, drawStatusMarker, err, f, logEvent, next, previous, progressIndicator, q, question, questionNumber, questions, results, staticDockerPs, statusMarker, _i, _len; 9 | 10 | COMPLETE_URL = "/whats-next/"; 11 | 12 | /* 13 | Array of question objects 14 | */ 15 | 16 | 17 | staticDockerPs = "ID IMAGE COMMAND CREATED STATUS PORTS"; 18 | 19 | q = []; 20 | 21 | q.push({ 22 | html: "

Getting started

\n

There are actually two programs: The Docker daemon, which is a server process and which manages all the\ncontainers, and the Docker client, which acts as a remote control on the daemon. On most systems, like in this\nemulator, both execute on the same host.

", 23 | assignment: "

Assignment

\n

Check which Docker versions are running

\n

This will help you verify the daemon is running and you can connect to it. If you see which version is running\nyou know you are all set.

", 24 | tip: "

Try typing docker to see the full list of accepted arguments

This emulator provides only a limited set of shell and Docker commands, so some commands may not work as expected

", 25 | command_expected: ['docker', 'version'], 26 | result: "

Well done! Let's move to the next assignment.

" 27 | }); 28 | 29 | q.push({ 30 | html: "

Searching for images

\n

The easiest way to get started is to use a container image from someone else. Container images are\navailable on the Docker index, a central place to store images. You can find them online at\nindex.docker.io\nand by using the commandline

", 31 | assignment: "

Assignment

\n

Use the commandline to search for an image called tutorial

", 32 | command_expected: ['docker', 'search', 'tutorial'], 33 | result: "

You found it!

", 34 | tip: "the format is docker search <string>" 35 | }); 36 | 37 | q.push({ 38 | html: "

Downloading container images

\n

Container images can be downloaded just as easily, using docker pull.

\n

For images from the central index, the name you specify is constructed as <username>/<repository>

\n

A group of special, trusted images such as the ubuntu base image can be retrieved by just their name <repository>.

", 39 | assignment: "

Assignment

\n

Please download the tutorial image you have just found

", 40 | command_expected: ['docker', 'pull', 'learn/tutorial'], 41 | result: "

Cool. Look at the results. You'll see that Docker has downloaded a number of layers. In Docker all images (except the base image) are made up of several cumulative layers.

", 42 | tip: "

Don't forget to pull the full name of the repository e.g. 'learn/tutorial'

\n

Look under 'show expected command if you're stuck.

" 43 | }); 44 | 45 | q.push({ 46 | html: "

Hello world from a container

\n

You can think about containers as a process in a box. The box contains everything the process might need, so\nit has the filesystem, system libraries, shell and such, but by default none of it is started or run.

\n

You 'start' a container by running a process in it. This process is the only process run, so when\nit completes the container is fully stopped.", 47 | assignment: "

Assignment

\n

Make our freshly loaded container image output \"hello world\"

\n

To do so you should run 'echo' in the container and have that say \"hello world\"\n", 48 | command_expected: ["docker", "run", "learn/tutorial", "echo", "hello"], 49 | command_show: ["docker", "run", "learn/tutorial", 'echo "hello world"'], 50 | result: "

Great! Hellooooo World!

You have just started a container and executed a program inside of it, when\nthe program stopped, so did the container.", 51 | intermediateresults: [ 52 | function() { 53 | return "

You seem to be almost there. Did you give the command `echo \"hello world\"` "; 54 | }, function() { 55 | return "

You've got the arguments right. Did you get the command? Try /bin/bash ?

"; 56 | } 57 | ], 58 | tip: "

The command docker run takes a minimum of two arguments. An image name, and the command you want to execute\nwithin that image.

\n

Check the expected command below if it does not work as expected

" 59 | }); 60 | 61 | q.push({ 62 | html: "

Installing things in the container

\n

Next we are going to install a simple program (ping) in the container. The image is based upon ubuntu, so you\ncan run the command apt-get install -y ping in the container.

\n

Note that even though the container stops right after a command completes, the changes are not forgotten.

", 63 | assignment: "

Assignment

\n

Install 'ping' on top of the learn/tutorial image.

", 64 | command_expected: ["docker", "run", "learn/tutorial", "apt-get", "install", "-y", "ping"], 65 | result: "

That worked! You have installed a program on top of a base image. Your changes to the filesystem have been\nkept, but are not yet saved.

", 66 | intermediateresults: [ 67 | function() { 68 | return "

Not specifying -y on the apt-get install command will work for ping, because it has no other dependencies, but\nit will fail when apt-get wants to install dependencies. To get into the habit, please add -y after apt-get.

"; 69 | } 70 | ], 71 | tip: "

Don't forget to use -y for noninteractive mode installation

\n

Not specifying -y on the apt-get install command will fail for most commands because it expects you to accept\n(y/n) but you cannot respond.\n

" 72 | }); 73 | 74 | q.push({ 75 | html: "

Save your changes

\n

After you make changes (by running a command inside a container), you probably want to save those changes.\nThis will enable you to later start from this point onwards.

\n

With Docker, the process of saving the state is called committing. Commit basically saves the difference\nbetween the old image and the new state. The result is a new layer.

", 76 | assignment: "

Assignment

\n

First use docker ps -l to find the ID of the container you created by installing ping.

\n

Then save (commit) this container with the repository name 'learn/ping'

", 77 | command_expected: ["docker", "commit", "698", "learn/ping"], 78 | command_show: ["docker", "commit", "698", 'learn/ping'], 79 | result: "

That worked! Please take note that Docker has returned a new ID. This id is the image id.

", 80 | intermediateresults: [ 81 | function() { 82 | return "You have not specified the correct repository name to commit to (learn/ping). This works, but giving your images a name\nmakes them much easier to work with."; 83 | } 84 | ], 85 | tip: "" 86 | }); 87 | 88 | q.push({ 89 | html: "

Run your new image

\n

Now you have basically setup a complete, self contained environment with the 'ping' program installed.

\n

Your image can now be run on any host that runs Docker.

\n

Lets run this image on this machine.

", 90 | assignment: "

Assignment

\n

Run the ping program to ping www.google.com

\n", 91 | command_expected: ["docker", "run", 'learn/ping', 'ping', 'google.com'], 92 | result: "

That worked! Note that normally you can use Ctrl-C to disconnect. The container will keep running. This\ncontainer will disconnect automatically.

", 93 | intermediateresults: [ 94 | function() { 95 | return "You have not specified a repository name. This is not wrong, but giving your images a name\nmake them much easier to work with."; 96 | } 97 | ], 98 | tip: "" 99 | }); 100 | 101 | q.push({ 102 | html: "

Check your running image

\n

You now have a running container. Let's see what is going on.

\n

Using docker ps we can see a list of all running containers, and using docker inspect\nwe can see all sorts of useful information about this container.

", 103 | assignment: "

Assignment

\n

Find the container id of the running container, and then inspect the container using docker inspect.

\n", 104 | command_expected: ["docker", "inspect", "efe"], 105 | result: "

Success! Have a look at the output. You can see the ip-address, status and other information.

", 106 | intermediateresults: [ 107 | function() { 108 | return "You have not specified a repository name. This is not wrong, but giving your images a name\nmake them much easier to work with."; 109 | } 110 | ], 111 | tip: "", 112 | currentDockerPs: "ID IMAGE COMMAND CREATED STATUS PORTS\nefefdc74a1d5 learn/ping:latest ping www.google.com 37 seconds ago Up 36 seconds" 113 | }); 114 | 115 | q.push({ 116 | html: "

Push your image to the index

\n

Now you have verified that your application container works, you can share it.

\n

Remember you pulled (downloaded) the learn/tutorial image from the index? You can also share your built images\nto the index by pushing (uploading) them to there. That way you can easily retrieve them for re-use and share them\nwith others.

", 117 | assignment: "

Assignment

\n

Push your container image learn/ping to the index

\n", 118 | command_expected: ["will_never_be_valid"], 119 | command_show: ["docker", "push", "learn/ping"], 120 | result: "", 121 | intermediateresults: [ 122 | function() { 123 | var data; 124 | 125 | $('#instructions .assignment').hide(); 126 | $('#tips, #command').hide(); 127 | $('#instructions .text').html("
\n

Congratulations!

\n

You have mastered the basic docker commands!

\n

Did you enjoy this tutorial? Share it!

\n

\n \n \n \n

\n

Your next steps

\n
    \n
  1. Register for news and updates on Docker (opens in new window)
  2. \n
  3. Follow us on twitter (opens in new window)
  4. \n
  5. Close this tutorial, and continue with the rest of the getting started.
  6. \n
\n

- Or -

\n

Continue to learn about the way to automatically build your containers from a file.

Start Dockerfile tutorial

\n\n
"); 128 | data = { 129 | type: EVENT_TYPES.complete 130 | }; 131 | logEvent(data); 132 | return "

All done!. You are now pushing a container image to the index. You can see that push, just like pull, happens layer by layer.

"; 133 | } 134 | ], 135 | tip: "", 136 | finishedCallback: function() { 137 | webterm.clear(); 138 | return webterm.echo(myTerminal()); 139 | } 140 | }); 141 | 142 | questions = []; 143 | 144 | /* 145 | Register the terminal 146 | */ 147 | 148 | 149 | this.webterm = $('#terminal').terminal(interpreter, basesettings); 150 | 151 | EVENT_TYPES = { 152 | none: "none", 153 | start: "start", 154 | command: "command", 155 | next: "next", 156 | peek: "peek", 157 | feedback: "feedback", 158 | complete: "complete" 159 | }; 160 | 161 | /* 162 | Sending events to the server 163 | */ 164 | 165 | 166 | logEvent = function(data, feedback) { 167 | var ajax_load, callback, loadUrl; 168 | 169 | ajax_load = "loading......"; 170 | loadUrl = "/tutorial/api/"; 171 | if (!feedback) { 172 | callback = function(responseText) { 173 | return $("#ajax").html(responseText); 174 | }; 175 | } else { 176 | callback = function(responseText) { 177 | results.set("Thank you for your feedback! We appreciate it!", true); 178 | $('#feedbackInput').val(""); 179 | return $("#ajax").html(responseText); 180 | }; 181 | } 182 | if (!data) { 183 | data = { 184 | type: EVENT_TYPES.none 185 | }; 186 | } 187 | data.question = current_question; 188 | $("#ajax").html(ajax_load); 189 | return $.post(loadUrl, data, callback, "html"); 190 | }; 191 | 192 | /* 193 | Event handlers 194 | */ 195 | 196 | 197 | $('#buttonNext').click(function(e) { 198 | this.setAttribute('disabled', 'disabled'); 199 | console.log(e); 200 | return next(); 201 | }); 202 | 203 | $('#buttonFinish').click(function() { 204 | return window.open(COMPLETE_URL); 205 | }); 206 | 207 | $('#buttonPrevious').click(function() { 208 | previous(); 209 | return $('#results').hide(); 210 | }); 211 | 212 | $('#leftside').bind('mousewheel', function(event, delta, deltaX, deltaY) { 213 | this.scrollTop += deltaY * -30; 214 | return event.preventDefault(); 215 | }); 216 | 217 | $('#feedbackSubmit').click(function() { 218 | var data, feedback; 219 | 220 | feedback = $('#feedbackInput').val(); 221 | data = { 222 | type: EVENT_TYPES.feedback, 223 | feedback: feedback 224 | }; 225 | return logEvent(data, feedback = true); 226 | }); 227 | 228 | $('#fullSizeOpen').click(function() { 229 | return goFullScreen(); 230 | }); 231 | 232 | this.goFullScreen = function() { 233 | console.debug("going to fullsize mode"); 234 | $('.togglesize').removeClass('startsize').addClass('fullsize'); 235 | $('.hide-when-small').css({ 236 | display: 'inherit' 237 | }); 238 | $('.hide-when-full').css({ 239 | display: 'none' 240 | }); 241 | next(0); 242 | webterm.resize(); 243 | return setTimeout(function() { 244 | return logEvent({ 245 | type: EVENT_TYPES.start 246 | }); 247 | }, 3000); 248 | }; 249 | 250 | $('#fullSizeClose').click(function() { 251 | return leaveFullSizeMode(); 252 | }); 253 | 254 | this.leaveFullSizeMode = function() { 255 | console.debug("leaving full-size mode"); 256 | $('.togglesize').removeClass('fullsize').addClass('startsize'); 257 | $('.hide-when-small').css({ 258 | display: 'none' 259 | }); 260 | $('.hide-when-full').css({ 261 | display: 'inherit' 262 | }); 263 | return webterm.resize(); 264 | }; 265 | 266 | $('#command').click(function() { 267 | var data; 268 | 269 | if (!$('#commandHiddenText').hasClass('hidden')) { 270 | $('#commandHiddenText').addClass("hidden").hide(); 271 | $('#commandShownText').hide().removeClass("hidden").fadeIn(); 272 | } 273 | data = { 274 | type: EVENT_TYPES.peek 275 | }; 276 | return logEvent(data); 277 | }); 278 | 279 | /* 280 | Navigation amongst the questions 281 | */ 282 | 283 | 284 | current_question = 0; 285 | 286 | next = function(which) { 287 | var data; 288 | 289 | $('#marker-' + current_question).addClass("complete").removeClass("active"); 290 | if (!which && which !== 0) { 291 | current_question++; 292 | } else { 293 | current_question = which; 294 | } 295 | questions[current_question](); 296 | results.clear(); 297 | this.webterm.focus(); 298 | if (!$('#commandShownText').hasClass('hidden')) { 299 | $('#commandShownText').addClass("hidden"); 300 | $('#commandHiddenText').removeClass("hidden").show(); 301 | } 302 | history.pushState({}, "", "#" + current_question); 303 | data = { 304 | 'type': EVENT_TYPES.next 305 | }; 306 | logEvent(data); 307 | $('#marker-' + current_question).removeClass("complete").addClass("active"); 308 | $('#question-number').find('text').get(0).textContent = current_question; 309 | $('#instructions .assignment').show(); 310 | $('#tips, #command').show(); 311 | }; 312 | 313 | previous = function() { 314 | current_question--; 315 | questions[current_question](); 316 | results.clear(); 317 | this.webterm.focus(); 318 | }; 319 | 320 | results = { 321 | set: function(htmlText, intermediate) { 322 | if (intermediate) { 323 | console.debug("intermediate text received"); 324 | $('#results').addClass('intermediate'); 325 | $('#buttonNext').hide(); 326 | } else { 327 | $('#buttonNext').show(); 328 | } 329 | return window.setTimeout((function() { 330 | $('#resulttext').html(htmlText); 331 | $('#results').fadeIn(); 332 | return $('#buttonNext').removeAttr('disabled'); 333 | }), 300); 334 | }, 335 | clear: function() { 336 | $('#resulttext').html(""); 337 | return $('#results').fadeOut('slow'); 338 | } 339 | }; 340 | 341 | /* 342 | Transform question objects into functions 343 | */ 344 | 345 | 346 | buildfunction = function(q) { 347 | var _q; 348 | 349 | _q = q; 350 | return function() { 351 | console.debug("function called"); 352 | $('#instructions').hide().fadeIn(); 353 | $('#instructions .text').html(_q.html); 354 | $('#instructions .assignment').html(_q.assignment); 355 | $('#tipShownText').html(_q.tip); 356 | if (_q.command_show) { 357 | $('#commandShownText').html(_q.command_show.join(' ')); 358 | } else { 359 | $('#commandShownText').html(_q.command_expected.join(' ')); 360 | } 361 | if (_q.currentDockerPs != null) { 362 | window.currentDockerPs = _q.currentDockerPs; 363 | } else { 364 | window.currentDockerPs = staticDockerPs; 365 | } 366 | if (_q.finishedCallback != null) { 367 | window.finishedCallback = q.finishedCallback; 368 | } else { 369 | window.finishedCallback = function() { 370 | return ""; 371 | }; 372 | } 373 | window.immediateCallback = function(input, stop) { 374 | var data, doNotExecute; 375 | 376 | if (stop === true) { 377 | doNotExecute = true; 378 | } else { 379 | doNotExecute = false; 380 | } 381 | if (doNotExecute !== true) { 382 | console.log(input); 383 | data = { 384 | 'type': EVENT_TYPES.command, 385 | 'command': input.join(' '), 386 | 'result': 'fail' 387 | }; 388 | if (input.containsAllOfTheseParts(_q.command_expected)) { 389 | data.result = 'success'; 390 | setTimeout((function() { 391 | this.webterm.disable(); 392 | return $('#buttonNext').focus(); 393 | }), 1000); 394 | results.set(_q.result); 395 | console.debug("contains match"); 396 | } else { 397 | console.debug("wrong command received"); 398 | } 399 | logEvent(data); 400 | } 401 | }; 402 | window.intermediateResults = function(input) { 403 | var intermediate; 404 | 405 | if (_q.intermediateresults) { 406 | return results.set(_q.intermediateresults[input](), intermediate = true); 407 | } 408 | }; 409 | }; 410 | }; 411 | 412 | statusMarker = $('#progress-marker-0'); 413 | 414 | progressIndicator = $('#progress-indicator'); 415 | 416 | drawStatusMarker = function(i) { 417 | var marker; 418 | 419 | if (i === 0) { 420 | marker = statusMarker; 421 | } else { 422 | marker = statusMarker.clone(); 423 | marker.appendTo(progressIndicator); 424 | } 425 | marker.attr("id", "marker-" + i); 426 | marker.find('text').get(0).textContent = i; 427 | return marker.click(function() { 428 | return next(i); 429 | }); 430 | }; 431 | 432 | questionNumber = 0; 433 | 434 | for (_i = 0, _len = q.length; _i < _len; _i++) { 435 | question = q[_i]; 436 | f = buildfunction(question); 437 | questions.push(f); 438 | drawStatusMarker(questionNumber); 439 | questionNumber++; 440 | } 441 | 442 | /* 443 | Initialization of program 444 | */ 445 | 446 | 447 | if (window.location.hash) { 448 | try { 449 | currentquestion = window.location.hash.split('#')[1].toNumber(); 450 | next(currentquestion); 451 | } catch (_error) { 452 | err = _error; 453 | questions[0](); 454 | } 455 | } else { 456 | questions[0](); 457 | } 458 | 459 | $('#results').hide(); 460 | 461 | }).call(this); 462 | -------------------------------------------------------------------------------- /docker_tutorial/static/js/terminal.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Please note the javascript is being fully generated from coffeescript. 3 | So make your changes in the .coffee file. 4 | Thatcher Peskens 5 | _ 6 | ,_(')< 7 | \___) 8 | 9 | ### 10 | 11 | do @myTerminal = -> 12 | 13 | # Which terminal emulator version are we 14 | EMULATOR_VERSION = "0.1.3" 15 | 16 | 17 | @basesettings = { 18 | prompt: 'you@tutorial:~$ ', 19 | greetings: """ 20 | Welcome to the interactive Docker tutorial 21 | """ 22 | 23 | } 24 | 25 | ### 26 | Callback definitions. These can be overridden by functions anywhere else 27 | ### 28 | 29 | @preventDefaultCallback = false 30 | 31 | @immediateCallback = (command) -> 32 | console.debug("immediate callback from #{command}") 33 | return 34 | 35 | @finishedCallback = (command) -> 36 | console.debug("finished callback from #{command}") 37 | return 38 | 39 | @intermediateResults = (string) -> 40 | console.debug("sent #{string}") 41 | return 42 | 43 | @currentDockerPs = "" 44 | 45 | ### 46 | Base interpreter 47 | ### 48 | 49 | @interpreter = (input, term) -> 50 | inputs = input.split(" ") 51 | command = inputs[0] 52 | 53 | if command is 'hi' 54 | term.echo 'hi there! What is your name??' 55 | term.push (command, term) -> 56 | term.echo command + ' is a pretty name' 57 | 58 | else if command is 'shell' 59 | term.push (command, term) -> 60 | if command is 'cd' 61 | bash(term, inputs) 62 | , {prompt: '> $ '} 63 | 64 | else if command is 'r' 65 | location.reload('forceGet') 66 | 67 | else if command is '#' 68 | term.echo 'which question?' 69 | 70 | else if command is 'test' 71 | term.echo 'I have to keep testing myself.' 72 | 73 | else if command is 'cd' 74 | bash(term, inputs) 75 | 76 | else if command is "docker" 77 | Docker(term, inputs) 78 | 79 | else if command is "help" 80 | term.echo help 81 | 82 | else if command is "ls" 83 | term.echo "This is an emulator, not a shell. Try following the instructions." 84 | 85 | else if command is "colors" 86 | for DockerCommand, description of DockerCommands 87 | term.echo ("[[b;#fff;]" + DockerCommand + "] - " + description + "") 88 | 89 | else if command is "pull" 90 | term.echo '[[b;#fff;]some text]' 91 | wait(term, 5000, true) 92 | alert term.get_output() 93 | 94 | return 95 | 96 | ## finally 97 | else if command 98 | term.echo "#{inputs[0]}: command not found" 99 | 100 | immediateCallback(inputs) 101 | 102 | ### ======================================= 103 | Common utils 104 | ======================================= ### 105 | 106 | String.prototype.beginsWith = (string) -> 107 | ### 108 | Check if 'this' string starts with the inputstring. 109 | ### 110 | return(this.indexOf(string) is 0) 111 | 112 | Array.prototype.containsAllOfThese = (inputArr) -> 113 | ### 114 | This function compares all of the elements in the inputArr 115 | and checks them one by one if they exist in 'this'. When it 116 | finds an element to not exist, it returns false. 117 | ### 118 | me = this 119 | valid = false 120 | 121 | if inputArr 122 | valid = inputArr.every( (value) -> 123 | if me.indexOf(value) == -1 124 | return false 125 | else 126 | return true 127 | ) 128 | return valid 129 | 130 | 131 | Array.prototype.containsAllOfTheseParts = (inputArr) -> 132 | ### 133 | This function is like containsAllofThese, but also matches partial strings. 134 | ### 135 | 136 | me = this 137 | if inputArr 138 | valid = inputArr.every( (value) -> 139 | for item in me 140 | if item.match(value) 141 | return true 142 | 143 | return false 144 | ) 145 | return valid 146 | 147 | 148 | parseInput = (inputs) -> 149 | command = inputs[1] 150 | switches = [] 151 | switchArg = false 152 | switchArgs = [] 153 | imagename = "" 154 | commands = [] 155 | j = 0 156 | 157 | # parse args 158 | for input in inputs 159 | if input.startsWith('-') and imagename == "" 160 | switches.push(input) 161 | if switches.length > 0 162 | if not ['-i', '-t', '-d'].containsAllOfThese([input]) 163 | switchArg = true 164 | else if switchArg == true 165 | # reset switchArg 166 | switchArg = false 167 | switchArgs.push(input) 168 | else if j > 1 and imagename == "" 169 | # match wrong names 170 | imagename = input 171 | else if imagename != "" 172 | commands.push (input) 173 | else 174 | # nothing? 175 | j++ 176 | 177 | parsed_input = { 178 | 'switches': switches.sortBy(), 179 | 'switchArgs': switchArgs, 180 | 'imageName': imagename, 181 | 'commands': commands, 182 | } 183 | return parsed_input 184 | 185 | 186 | util_slow_lines = (term, paragraph, keyword, finishedCallback) -> 187 | 188 | if keyword 189 | lines = paragraph(keyword).split("\n") 190 | else 191 | lines = paragraph.split("\n") 192 | 193 | term.pause() 194 | i = 0 195 | # function calls itself after timeout is done, untill 196 | # all lines are finished 197 | foo = (lines) -> 198 | self.setTimeout ( -> 199 | if lines[i] 200 | term.echo (lines[i]) 201 | i++ 202 | foo(lines) 203 | else 204 | term.resume() 205 | finishedCallback() 206 | ), 1000 207 | 208 | foo(lines) 209 | 210 | 211 | wait = (term, time, dots) -> 212 | term.echo "starting to wait" 213 | interval_id = self.setInterval ( -> dots ? term.insert '.'), 500 214 | 215 | self.setTimeout ( -> 216 | self.clearInterval(interval_id) 217 | output = term.get_command() 218 | term.echo output 219 | term.echo "done " 220 | ), time 221 | 222 | ### 223 | Bash program 224 | ### 225 | 226 | bash = (term, inputs) -> 227 | echo = term.echo 228 | insert = term.insert 229 | 230 | if not inputs[1] 231 | console.log("none") 232 | 233 | else 234 | argument = inputs[1] 235 | if argument.beginsWith('..') 236 | echo "-bash: cd: #{argument}: Permission denied" 237 | else 238 | echo "-bash: cd: #{argument}: No such file or directory" 239 | 240 | ### 241 | Docker program 242 | ### 243 | 244 | Docker = (term, inputs) -> 245 | 246 | echo = term.echo 247 | insert = term.insert 248 | callback = () -> @finishedCallback(inputs) 249 | command = inputs[1] 250 | 251 | # no command 252 | if not inputs[1] 253 | console.debug "no args" 254 | echo Docker_cmd 255 | for DockerCommand, description of DockerCommands 256 | echo "[[b;#fff;]" + DockerCommand + "]" + description + "" 257 | 258 | # Command commit 259 | else if inputs[1] is "commit" 260 | if inputs.containsAllOfTheseParts(['docker', 'commit', '698', 'learn/ping']) 261 | util_slow_lines(term, commit_containerid, "", callback ) 262 | else if inputs.containsAllOfTheseParts(['docker', 'commit', '698']) 263 | util_slow_lines(term, commit_containerid, "", callback ) 264 | intermediateResults(0) 265 | else if inputs.containsAllOfTheseParts(['docker', 'commit']) and inputs[2] 266 | echo commit_id_does_not_exist(inputs[2]) 267 | else 268 | echo commit 269 | 270 | else if inputs[1] is "do" 271 | term.push('do', {prompt: "do $ "}) 272 | 273 | else if inputs[1] is "logo" 274 | echo Docker_logo 275 | 276 | else if inputs[1] is "images" 277 | echo images 278 | 279 | else if inputs[1] is "inspect" 280 | if inputs[2] and inputs[2].match('ef') 281 | echo inspect_ping_container 282 | else if inputs[2] 283 | echo inspect_no_such_container(inputs[2]) 284 | else 285 | echo inspect 286 | 287 | # command ps 288 | else if command is "ps" 289 | if inputs.containsAllOfThese(['-l']) 290 | echo ps_l 291 | else if inputs.containsAllOfThese(['-a']) 292 | echo ps_a 293 | else 294 | echo currentDockerPs 295 | else if inputs[1] is "push" 296 | if inputs[2] is "learn/ping" 297 | util_slow_lines(term, push_container_learn_ping, "", callback ) 298 | intermediateResults(0) 299 | return 300 | else if inputs[2] 301 | echo push_wrong_name 302 | else 303 | echo push 304 | 305 | 306 | # Command run 307 | else if inputs[1] is "run" 308 | # parse all input so we have a json object 309 | parsed_input = parseInput(inputs) 310 | 311 | switches = parsed_input.switches 312 | swargs = parsed_input.switchArgs 313 | imagename = parsed_input.imageName 314 | commands = parsed_input.commands 315 | 316 | console.log "commands" 317 | console.log commands 318 | console.log "switches" 319 | console.log switches 320 | 321 | console.log "parsed input" 322 | console.log parsed_input 323 | console.log "imagename: #{imagename}" 324 | 325 | if imagename is "ubuntu" 326 | if switches.containsAllOfTheseParts(['-i', '-t']) 327 | if commands.containsAllOfTheseParts(['bash']) 328 | term.push ( (command, term) -> 329 | if command 330 | echo """this shell is not implemented. Enter 'exit' to exit.""" 331 | return 332 | ), {prompt: 'root@687bbbc4231b:/# '} 333 | else 334 | echo run_image_wrong_command(commands) 335 | else 336 | echo run_flag_defined_not_defined(switches) 337 | else if imagename is "learn/tutorial" 338 | if switches.length > 0 339 | echo run_learn_no_command 340 | intermediateResults(0) 341 | else if commands[0] is "/bin/bash" 342 | echo run_learn_tutorial_echo_hello_world(commands) 343 | intermediateResults(2) 344 | else if commands[0] is "echo" 345 | echo run_learn_tutorial_echo_hello_world(commands) 346 | else if commands.containsAllOfThese(['apt-get', 'install', '-y', 'iputils-ping']) 347 | echo run_apt_get_install_iputils_ping 348 | else if commands.containsAllOfThese(['apt-get', 'install', 'iputils-ping']) 349 | echo run_apt_get_install_iputils_ping 350 | # intermediateResults(0) 351 | else if commands.containsAllOfThese(['apt-get', 'install', 'ping']) 352 | echo run_apt_get_install_iputils_ping 353 | # intermediateResults(0) 354 | else if commands.containsAllOfThese(['apt-get', 'install']) 355 | i = commands.length - 1 356 | echo run_apt_get_install_unknown_package( commands[i] ) 357 | # intermediateResults(0) 358 | else if commands[0] is "apt-get" 359 | echo run_apt_get 360 | else if commands[0] 361 | echo run_image_wrong_command(commands[0]) 362 | else 363 | echo run_learn_no_command 364 | 365 | else if imagename is "learn/ping" 366 | if commands.containsAllOfTheseParts(["ping", "google.com"]) 367 | util_slow_lines(term, run_ping_www_google_com, "", callback ) 368 | else if commands[0] is "ping" and commands[1] 369 | echo run_ping_not_google(commands[1]) 370 | else if commands[0] is "ping" 371 | echo ping 372 | else if commands[0] 373 | echo "#{commands[0]}: command not found" 374 | else 375 | echo run_learn_no_command 376 | 377 | else if imagename 378 | echo run_notfound(inputs[2]) 379 | else 380 | console.log("run") 381 | echo run_cmd 382 | 383 | else if inputs[1] is "search" 384 | if keyword = inputs[2] 385 | if keyword is "ubuntu" 386 | echo search_ubuntu 387 | else if keyword is "tutorial" 388 | echo search_tutorial 389 | else 390 | echo search_no_results(inputs[2]) 391 | else echo search 392 | 393 | else if inputs[1] is "pull" 394 | if keyword = inputs[2] 395 | if keyword is 'ubuntu' 396 | result = util_slow_lines(term, pull_ubuntu, "", callback ) 397 | else if keyword is 'learn/tutorial' 398 | result = util_slow_lines(term, pull_tutorial, "", callback ) 399 | else 400 | util_slow_lines(term, pull_no_results, keyword) 401 | else 402 | echo pull 403 | 404 | else if inputs[1] is "version" 405 | # console.log(version) 406 | echo docker_version() 407 | 408 | 409 | else if DockerCommands[inputs[1]] 410 | echo "#{inputs[1]} is a valid argument, but not implemented" 411 | 412 | else 413 | echo Docker_cmd 414 | for DockerCommand, description of DockerCommands 415 | echo "[[b;#fff;]" + DockerCommand + "]" + description + "" 416 | 417 | # return empty value because otherwise coffeescript will return last var 418 | return 419 | 420 | ### 421 | Some default variables / commands 422 | 423 | All items are sorted by alphabet 424 | ### 425 | 426 | Docker_cmd = \ 427 | """ 428 | Usage: Docker [OPTIONS] COMMAND [arg...] 429 | -H="127.0.0.1:4243": Host:port to bind/connect to 430 | 431 | A self-sufficient runtime for linux containers. 432 | 433 | Commands: 434 | 435 | """ 436 | 437 | DockerCommands = 438 | "attach": " Attach to a running container" 439 | "build": " Build a container from a Dockerfile" 440 | "commit": " Create a new image from a container's changes" 441 | "diff": " Inspect changes on a container's filesystem" 442 | "export": " Stream the contents of a container as a tar archive" 443 | "history": " Show the history of an image" 444 | "images": " List images" 445 | "import": " Create a new filesystem image from the contents of a tarball" 446 | "info": " Display system-wide information" 447 | "insert": " Insert a file in an image" 448 | "inspect": " Return low-level information on a container" 449 | "kill": " Kill a running container" 450 | "login": " Register or Login to the Docker registry server" 451 | "logs": " Fetch the logs of a container" 452 | "port": " Lookup the public-facing port which is NAT-ed to PRIVATE_PORT" 453 | "ps": " List containers" 454 | "pull": " Pull an image or a repository from the Docker registry server" 455 | "push": " Push an image or a repository to the Docker registry server" 456 | "restart": " Restart a running container" 457 | "rm": " Remove a container" 458 | "rmi": " Remove an image" 459 | "run": " Run a command in a new container" 460 | "search": " Search for an image in the Docker index" 461 | "start": " Start a stopped container" 462 | "stop": " Stop a running container" 463 | "tag": " Tag an image into a repository" 464 | "version": " Show the Docker version information" 465 | "wait": " Block until a container stops, then print its exit code" 466 | 467 | run_switches = 468 | "-p": ['port'] 469 | "-t": [] 470 | "-i": [] 471 | "-h": ['hostname'] 472 | 473 | commit = \ 474 | """ 475 | Usage: Docker commit [OPTIONS] CONTAINER [REPOSITORY [TAG]] 476 | 477 | Create a new image from a container's changes 478 | 479 | -author="": Author (eg. "John Hannibal Smith " 480 | -m="": Commit message 481 | -run="": Config automatically applied when the image is run. (ex: {"Cmd": ["cat", "/world"], "PortSpecs": ["22"]}') 482 | """ 483 | 484 | commit_id_does_not_exist = (keyword) -> 485 | """ 486 | 2013/07/08 23:51:21 Error: No such container: #{keyword} 487 | """ 488 | 489 | commit_containerid = \ 490 | """ 491 | effb66b31edb 492 | """ 493 | 494 | help = \ 495 | " 496 | Docker tutorial \n 497 | \n 498 | The Docker tutorial is a Docker emulater intended to help novice users get up to spead with the standard Docker 499 | commands. This terminal contains a limited Docker and a limited shell emulator. Therefore some of the commands 500 | you would expect do not exist.\n 501 | \n 502 | Just follow the steps and questions. If you are stuck, click on the 'expected command' to see what the command 503 | should have been. Leave feedback if you find things confusing. 504 | 505 | " 506 | 507 | images = \ 508 | """ 509 | ubuntu latest 8dbd9e392a96 4 months ago 131.5 MB (virtual 131.5 MB) 510 | learn/tutorial latest 8dbd9e392a96 2 months ago 131.5 MB (virtual 131.5 MB) 511 | learn/ping latest effb66b31edb 10 minutes ago 11.57 MB (virtual 143.1 MB) 512 | """ 513 | 514 | inspect = \ 515 | """ 516 | 517 | Usage: Docker inspect CONTAINER|IMAGE [CONTAINER|IMAGE...] 518 | 519 | Return low-level information on a container/image 520 | 521 | """ 522 | 523 | inspect_no_such_container = (keyword) -> 524 | """ 525 | Error: No such image: #{keyword} 526 | """ 527 | 528 | inspect_ping_container = \ 529 | """ 530 | [2013/07/30 01:52:26 GET /v1.3/containers/efef/json 531 | { 532 | "ID": "efefdc74a1d5900d7d7a74740e5261c09f5f42b6dae58ded6a1fde1cde7f4ac5", 533 | "Created": "2013-07-30T00:54:12.417119736Z", 534 | "Path": "ping", 535 | "Args": [ 536 | "www.google.com" 537 | ], 538 | "Config": { 539 | "Hostname": "efefdc74a1d5", 540 | "User": "", 541 | "Memory": 0, 542 | "MemorySwap": 0, 543 | "CpuShares": 0, 544 | "AttachStdin": false, 545 | "AttachStdout": true, 546 | "AttachStderr": true, 547 | "PortSpecs": null, 548 | "Tty": false, 549 | "OpenStdin": false, 550 | "StdinOnce": false, 551 | "Env": null, 552 | "Cmd": [ 553 | "ping", 554 | "www.google.com" 555 | ], 556 | "Dns": null, 557 | "Image": "learn/ping", 558 | "Volumes": null, 559 | "VolumesFrom": "", 560 | "Entrypoint": null 561 | }, 562 | "State": { 563 | "Running": true, 564 | "Pid": 22249, 565 | "ExitCode": 0, 566 | "StartedAt": "2013-07-30T00:54:12.424817715Z", 567 | "Ghost": false 568 | }, 569 | "Image": "a1dbb48ce764c6651f5af98b46ed052a5f751233d731b645a6c57f91a4cb7158", 570 | "NetworkSettings": { 571 | "IPAddress": "172.16.42.6", 572 | "IPPrefixLen": 24, 573 | "Gateway": "172.16.42.1", 574 | "Bridge": "docker0", 575 | "PortMapping": { 576 | "Tcp": {}, 577 | "Udp": {} 578 | } 579 | }, 580 | "SysInitPath": "/usr/bin/docker", 581 | "ResolvConfPath": "/etc/resolv.conf", 582 | "Volumes": {}, 583 | "VolumesRW": {} 584 | """ 585 | 586 | ping = \ 587 | """ 588 | Usage: ping [-LRUbdfnqrvVaAD] [-c count] [-i interval] [-w deadline] 589 | [-p pattern] [-s packetsize] [-t ttl] [-I interface] 590 | [-M pmtudisc-hint] [-m mark] [-S sndbuf] 591 | [-T tstamp-options] [-Q tos] [hop1 ...] destination 592 | """ 593 | 594 | ps = \ 595 | """ 596 | ID IMAGE COMMAND CREATED STATUS PORTS 597 | efefdc74a1d5 learn/ping:latest ping www.google.com 37 seconds ago Up 36 seconds 598 | """ 599 | 600 | ps_a = \ 601 | """ 602 | ID IMAGE COMMAND CREATED STATUS PORTS 603 | 6982a9948422 ubuntu:12.04 apt-get install ping 1 minute ago Exit 0 604 | efefdc74a1d5 learn/ping:latest ping www.google.com 37 seconds ago Up 36 seconds 605 | """ 606 | 607 | ps_l = \ 608 | """ 609 | ID IMAGE COMMAND CREATED STATUS PORTS 610 | 6982a9948422 ubuntu:12.04 apt-get install ping 1 minute ago Exit 0 611 | """ 612 | 613 | pull = \ 614 | """ 615 | Usage: Docker pull NAME 616 | 617 | Pull an image or a repository from the registry 618 | 619 | -registry="": Registry to download from. Necessary if image is pulled by ID 620 | -t="": Download tagged image in repository 621 | """ 622 | 623 | pull_no_results = (keyword) -> 624 | """ 625 | Pulling repository #{keyword} 626 | 2013/06/19 19:27:03 HTTP code: 404 627 | """ 628 | 629 | pull_ubuntu = 630 | """ 631 | Pulling repository ubuntu from https://index.docker.io/v1 632 | Pulling image 8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c (precise) from ubuntu 633 | Pulling image b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc (12.10) from ubuntu 634 | Pulling image 27cf784147099545 () from ubuntu 635 | """ 636 | 637 | pull_tutorial = \ 638 | """ 639 | Pulling repository learn/tutorial from https://index.docker.io/v1 640 | Pulling image 8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c (precise) from ubuntu 641 | Pulling image b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc (12.10) from ubuntu 642 | Pulling image 27cf784147099545 () from tutorial 643 | """ 644 | 645 | push = \ 646 | """ 647 | 648 | Usage: docker push NAME 649 | 650 | Push an image or a repository to the registry 651 | """ 652 | 653 | 654 | push_container_learn_ping = \ 655 | """ 656 | The push refers to a repository [learn/ping] (len: 1) 657 | Processing checksums 658 | Sending image list 659 | Pushing repository learn/ping (1 tags) 660 | Pushing 8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c 661 | Image 8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c already pushed, skipping 662 | Pushing tags for rev [8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c] on {https://registry-1.docker.io/v1/repositories/learn/ping/tags/latest} 663 | Pushing a1dbb48ce764c6651f5af98b46ed052a5f751233d731b645a6c57f91a4cb7158 664 | Pushing 11.5 MB/11.5 MB (100%) 665 | Pushing tags for rev [a1dbb48ce764c6651f5af98b46ed052a5f751233d731b645a6c57f91a4cb7158] on {https://registry-1.docker.io/v1/repositories/learn/ping/tags/latest} 666 | """ 667 | 668 | push_wrong_name = \ 669 | """ 670 | The push refers to a repository [dhrp/fail] (len: 0) 671 | """ 672 | 673 | run_cmd = \ 674 | """ 675 | Usage: Docker run [OPTIONS] IMAGE COMMAND [ARG...] 676 | 677 | Run a command in a new container 678 | 679 | -a=map[]: Attach to stdin, stdout or stderr. 680 | -c=0: CPU shares (relative weight) 681 | -d=false: Detached mode: leave the container running in the background 682 | -dns=[]: Set custom dns servers 683 | -e=[]: Set environment variables 684 | -h="": Container host name 685 | -i=false: Keep stdin open even if not attached 686 | -m=0: Memory limit (in bytes) 687 | -p=[]: Expose a container's port to the host (use 'docker port' to see the actual mapping) 688 | -t=false: Allocate a pseudo-tty 689 | -u="": Username or UID 690 | -v=map[]: Attach a data volume 691 | -volumes-from="": Mount volumes from the specified container 692 | """ 693 | 694 | run_apt_get = \ 695 | """ 696 | apt 0.8.16~exp12ubuntu10 for amd64 compiled on Apr 20 2012 10:19:39 697 | Usage: apt-get [options] command 698 | apt-get [options] install|remove pkg1 [pkg2 ...] 699 | apt-get [options] source pkg1 [pkg2 ...] 700 | 701 | apt-get is a simple command line interface for downloading and 702 | installing packages. The most frequently used commands are update 703 | and install. 704 | 705 | Commands: 706 | update - Retrieve new lists of packages 707 | upgrade - Perform an upgrade 708 | install - Install new packages (pkg is libc6 not libc6.deb) 709 | remove - Remove packages 710 | autoremove - Remove automatically all unused packages 711 | purge - Remove packages and config files 712 | source - Download source archives 713 | build-dep - Configure build-dependencies for source packages 714 | dist-upgrade - Distribution upgrade, see apt-get(8) 715 | dselect-upgrade - Follow dselect selections 716 | clean - Erase downloaded archive files 717 | autoclean - Erase old downloaded archive files 718 | check - Verify that there are no broken dependencies 719 | changelog - Download and display the changelog for the given package 720 | download - Download the binary package into the current directory 721 | 722 | Options: 723 | -h This help text. 724 | -q Loggable output - no progress indicator 725 | -qq No output except for errors 726 | -d Download only - do NOT install or unpack archives 727 | -s No-act. Perform ordering simulation 728 | -y Assume Yes to all queries and do not prompt 729 | -f Attempt to correct a system with broken dependencies in place 730 | -m Attempt to continue if archives are unlocatable 731 | -u Show a list of upgraded packages as well 732 | -b Build the source package after fetching it 733 | -V Show verbose version numbers 734 | -c=? Read this configuration file 735 | -o=? Set an arbitrary configuration option, eg -o dir::cache=/tmp 736 | See the apt-get(8), sources.list(5) and apt.conf(5) manual 737 | pages for more information and options. 738 | This APT has Super Cow Powers. 739 | 740 | """ 741 | 742 | run_apt_get_install_iputils_ping = \ 743 | """ 744 | Reading package lists... 745 | Building dependency tree... 746 | The following NEW packages will be installed: 747 | iputils-ping 748 | 0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded. 749 | Need to get 56.1 kB of archives. 750 | After this operation, 143 kB of additional disk space will be used. 751 | Get:1 http://archive.ubuntu.com/ubuntu/ precise/main iputils-ping amd64 3:20101006-1ubuntu1 [56.1 kB] 752 | debconf: delaying package configuration, since apt-utils is not installed 753 | Fetched 56.1 kB in 1s (50.3 kB/s) 754 | Selecting previously unselected package iputils-ping. 755 | (Reading database ... 7545 files and directories currently installed.) 756 | Unpacking iputils-ping (from .../iputils-ping_3%3a20101006-1ubuntu1_amd64.deb) ... 757 | Setting up iputils-ping (3:20101006-1ubuntu1) ... 758 | """ 759 | 760 | run_apt_get_install_unknown_package = (keyword) -> 761 | """ 762 | Reading package lists... 763 | Building dependency tree... 764 | E: Unable to locate package #{keyword} 765 | """ 766 | 767 | run_flag_defined_not_defined = (keyword) -> 768 | """ 769 | 2013/08/15 22:19:14 flag provided but not defined: #{keyword} 770 | """ 771 | 772 | run_learn_no_command = \ 773 | """ 774 | 2013/07/02 02:00:59 Error: No command specified 775 | """ 776 | 777 | run_learn_tutorial_echo_hello_world = (commands) -> 778 | string = "" 779 | for command in commands[1..] 780 | command = command.replace('"',''); 781 | string += ("#{command} ") 782 | return string 783 | 784 | 785 | run_image_wrong_command = (keyword) -> 786 | """ 787 | 2013/07/08 23:13:30 Unable to locate #{keyword} 788 | """ 789 | 790 | run_notfound = (keyword) -> 791 | """ 792 | Pulling repository #{keyword} from https://index.docker.io/v1 793 | 2013/07/02 02:14:47 Error: No such image: #{keyword} 794 | """ 795 | 796 | run_ping_not_google = (keyword) -> 797 | """ 798 | ping: unknown host #{keyword} 799 | """ 800 | 801 | run_ping_www_google_com = \ 802 | """ 803 | PING www.google.com (74.125.239.129) 56(84) bytes of data. 804 | 64 bytes from nuq05s02-in-f20.1e100.net (74.125.239.148): icmp_req=1 ttl=55 time=2.23 ms 805 | 64 bytes from nuq05s02-in-f20.1e100.net (74.125.239.148): icmp_req=2 ttl=55 time=2.30 ms 806 | 64 bytes from nuq05s02-in-f20.1e100.net (74.125.239.148): icmp_req=3 ttl=55 time=2.27 ms 807 | 64 bytes from nuq05s02-in-f20.1e100.net (74.125.239.148): icmp_req=4 ttl=55 time=2.30 ms 808 | 64 bytes from nuq05s02-in-f20.1e100.net (74.125.239.148): icmp_req=5 ttl=55 time=2.25 ms 809 | 64 bytes from nuq05s02-in-f20.1e100.net (74.125.239.148): icmp_req=6 ttl=55 time=2.29 ms 810 | 64 bytes from nuq05s02-in-f20.1e100.net (74.125.239.148): icmp_req=7 ttl=55 time=2.23 ms 811 | 64 bytes from nuq05s02-in-f20.1e100.net (74.125.239.148): icmp_req=8 ttl=55 time=2.30 ms 812 | 64 bytes from nuq05s02-in-f20.1e100.net (74.125.239.148): icmp_req=9 ttl=55 time=2.35 ms 813 | -> This would normally just keep going. However, this emulator does not support Ctrl-C, so we quit here. 814 | """ 815 | 816 | search = \ 817 | """ 818 | 819 | Usage: Docker search NAME 820 | 821 | Search the Docker index for images 822 | 823 | """ 824 | 825 | search_no_results = (keyword) -> 826 | """ 827 | Found 0 results matching your query ("#{keyword}") 828 | NAME DESCRIPTION 829 | """ 830 | 831 | search_tutorial = \ 832 | """ 833 | Found 1 results matching your query ("tutorial") 834 | NAME DESCRIPTION 835 | learn/tutorial An image for the interactive tutorial 836 | """ 837 | 838 | search_ubuntu = \ 839 | """ 840 | Found 22 results matching your query ("ubuntu") 841 | NAME DESCRIPTION 842 | shykes/ubuntu 843 | base Another general use Ubuntu base image. Tag... 844 | ubuntu General use Ubuntu base image. Tags availa... 845 | boxcar/raring Ubuntu Raring 13.04 suitable for testing v... 846 | dhrp/ubuntu 847 | creack/ubuntu Tags: 848 | 12.04-ssh, 849 | 12.10-ssh, 850 | 12.10-ssh-l... 851 | crohr/ubuntu Ubuntu base images. Only lucid (10.04) for... 852 | knewton/ubuntu 853 | pallet/ubuntu2 854 | erikh/ubuntu 855 | samalba/wget Test container inherited from ubuntu with ... 856 | creack/ubuntu-12-10-ssh 857 | knewton/ubuntu-12.04 858 | tithonium/rvm-ubuntu The base 'ubuntu' image, with rvm installe... 859 | dekz/build 13.04 ubuntu with build 860 | ooyala/test-ubuntu 861 | ooyala/test-my-ubuntu 862 | ooyala/test-ubuntu2 863 | ooyala/test-ubuntu3 864 | ooyala/test-ubuntu4 865 | ooyala/test-ubuntu5 866 | surma/go Simple augmentation of the standard Ubuntu... 867 | 868 | """ 869 | 870 | testing = \ 871 | """ 872 | Testing leads to failure, and failure leads to understanding. ~Burt Rutan 873 | """ 874 | 875 | docker_version = () -> 876 | """ 877 | Docker Emulator version #{EMULATOR_VERSION} 878 | 879 | Emulating: 880 | Client version: 0.5.3 881 | Server version: 0.5.3 882 | Go version: go1.1 883 | """ 884 | 885 | 886 | Docker_logo = \ 887 | ''' 888 | _ _ _ _ 889 | __ _____| | | __| | ___ _ __ ___ | | 890 | \\\ \\\ /\\\ / / _ \\\ | | / _` |/ _ \\\| '_ \\\ / _ \\\ | | 891 | \\\ V V / __/ | | | (_| | (_) | | | | __/ |_| 892 | \\\_/\\\_/ \\\___|_|_| \\\__,_|\\\___/|_| |_|\\\___| (_) 893 | 894 | 895 | 896 | 897 | ## . 898 | ## ## ## == 899 | ## ## ## ## === 900 | /""""""""""""""""\\\___/ === 901 | ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~ 902 | \\\______ o __/ 903 | \\\ \\\ __/ 904 | \\\____\\\______/ 905 | 906 | | | 907 | __ | __ __ | _ __ _ 908 | / \\\| / \\\ / |/ / _\\\ | 909 | \\\__/| \\\__/ \\\__ |\\\_ \\\__ | 910 | 911 | 912 | ''' 913 | 914 | 915 | return this 916 | 917 | 918 | -------------------------------------------------------------------------------- /docker_tutorial/static/lib/css/jquery.terminal.css: -------------------------------------------------------------------------------- 1 | .terminal .terminal-output .format, .terminal .cmd .format, 2 | .terminal .cmd .prompt, .terminal .cmd .prompt div, .terminal .terminal-output div div{ 3 | display: inline-block; 4 | } 5 | .terminal .clipboard { 6 | position: absolute; 7 | bottom: 0; 8 | left: 0; 9 | opacity: 0.01; 10 | filter: alpha(opacity = 0.01); 11 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=0.01); 12 | width: 2px; 13 | } 14 | .cmd > .clipboard { 15 | position: fixed; 16 | } 17 | .terminal { 18 | padding: 10px; 19 | position: relative; 20 | overflow: hidden; 21 | } 22 | .cmd { 23 | padding: 0; 24 | margin: 0; 25 | height: 1.3em; 26 | margin-top: 3px; 27 | } 28 | .terminal .terminal-output div div, .terminal .prompt { 29 | display: block; 30 | line-height: 14px; 31 | height: auto; 32 | } 33 | .terminal .prompt { 34 | float: left; 35 | } 36 | 37 | .terminal { 38 | font-family: FreeMono, monospace; 39 | color: #aaa; 40 | background-color: #000; 41 | font-size: 12px; 42 | line-height: 14px; 43 | } 44 | .terminal-output > div { 45 | padding-top: 3px; 46 | } 47 | .terminal .terminal-output div span { 48 | display: inline-block; 49 | } 50 | .terminal .cmd span { 51 | float: left; 52 | /*display: inline-block; */ 53 | } 54 | .terminal .cmd span.inverted { 55 | background-color: #aaa; 56 | color: #000; 57 | } 58 | .terminal .terminal-output div div::-moz-selection, 59 | .terminal .terminal-output div span::-moz-selection, 60 | .terminal .terminal-output div div a::-moz-selection { 61 | background-color: #aaa; 62 | color: #000; 63 | } 64 | .terminal .terminal-output div div::selection, 65 | .terminal .terminal-output div div a::selection, 66 | .terminal .terminal-output div span::selection, 67 | .terminal .cmd > span::selection, 68 | .terminal .prompt span::selection { 69 | background-color: #aaa; 70 | color: #000; 71 | } 72 | .terminal .terminal-output div.error, .terminal .terminal-output div.error div { 73 | color: red; 74 | } 75 | .tilda { 76 | position: fixed; 77 | top: 0; 78 | left: 0; 79 | width: 100%; 80 | z-index: 1100; 81 | } 82 | .clear { 83 | clear: both; 84 | } 85 | .terminal a { 86 | color: #0F60FF; 87 | } 88 | .terminal a:hover { 89 | color: red; 90 | } 91 | -------------------------------------------------------------------------------- /docker_tutorial/static/lib/js/jquery.mousewheel-min.js: -------------------------------------------------------------------------------- 1 | (function(c){function g(a){var b=a||window.event,i=[].slice.call(arguments,1),e=0,h=0,f=0;a=c.event.fix(b);a.type="mousewheel";if(b.wheelDelta)e=b.wheelDelta/120;if(b.detail)e=-b.detail/3;f=e;if(b.axis!==undefined&&b.axis===b.HORIZONTAL_AXIS){f=0;h=-1*e}if(b.wheelDeltaY!==undefined)f=b.wheelDeltaY/120;if(b.wheelDeltaX!==undefined)h=-1*b.wheelDeltaX/120;i.unshift(a,e,h,f);return(c.event.dispatch||c.event.handle).apply(this,i)}var d=["DOMMouseScroll","mousewheel"];if(c.event.fixHooks)for(var j=d.length;j;)c.event.fixHooks[d[--j]]= 2 | c.event.mouseHooks;c.event.special.mousewheel={setup:function(){if(this.addEventListener)for(var a=d.length;a;)this.addEventListener(d[--a],g,false);else this.onmousewheel=g},teardown:function(){if(this.removeEventListener)for(var a=d.length;a;)this.removeEventListener(d[--a],g,false);else this.onmousewheel=null}};c.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})})(jQuery); 3 | -------------------------------------------------------------------------------- /docker_tutorial/templates/tutorial/snippet.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 |
11 | Docker 12 |

Learn Docker

13 | 14 | 15 | 16 | 17 |
18 | 19 | > 21 | 22 | i 23 | 24 | 25 |
26 |
27 | 28 |
29 |
30 |
31 | 33 | 34 | 1 35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 |
47 | 49 | 50 | i 51 | 52 |
53 | 54 |
55 |

Tips

56 | {#
#} 57 | {# click here for tips#} 58 | {#
#} 59 |
60 | This is what the written tip looks like 61 |
62 |
63 |
64 | 65 | 66 |
67 |
68 |

Show expected command

69 |
70 | click here to see the expected command 71 |
72 | 75 |
76 |
77 | 78 | 79 | 80 |
81 | nothing here 82 |
83 |
84 | 85 |
86 |

Learn the first steps of using Docker, such as:

87 |
    88 |
  • Finding and downloading images
  • 89 |
  • Running hello world
  • 90 |
  • Committing your changes
  • 91 |
  • Pushing an image to the repository
  • 92 |
93 | 94 |
95 | Start! 96 |
97 |
98 | 99 |
100 |
101 |
102 | 103 |
104 | 106 | 107 | 108 | 109 | 110 |
111 |
112 | 113 | 114 | 115 |
116 | 117 | 121 | 122 | 123 | 124 |
125 |
126 |
127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /docker_tutorial/templates/tutorial/stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Docker tutorial stats 5 | 6 | 7 | 8 | 9 | 10 |

General numbers

11 | 16 | 17 | 18 |

How far did people get?

19 |
    20 | {% for question, number in answered.items %} 21 |
  1. {{ number }}
  2. 22 | {% endfor %} 23 |
24 | 25 | 26 | 27 |

How many people check for the answer, and which?

28 |
    29 | {% for question, number in peeks.items %} 30 |
  1. {{ number }}
  2. 31 | {% endfor %} 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /docker_tutorial/templates/tutorial/testpage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Docker tutorial testpage 5 | 6 | 7 | 8 | {% include 'tutorial/snippet.html' %} 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docker_tutorial/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhrp/docker-tutorial/6edf3d18e6a71a22fbd7bd1a0f371fee924d615a/docker_tutorial/tests/__init__.py -------------------------------------------------------------------------------- /docker_tutorial/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.test import TestCase 3 | 4 | 5 | class WhenGettingTheTutorialAPI(TestCase): 6 | 7 | def setUp(self): 8 | self.response = self.client.get(reverse("tutorial_api")) 9 | 10 | def test_that_the_response_is_successful(self): 11 | self.assertEqual(self.response.status_code, 200) 12 | 13 | 14 | class WhenPostingToTheAPI(TestCase): 15 | 16 | def setUp(self): 17 | self.response = self.client.post(reverse("tutorial_api")) 18 | 19 | def test_that_the_response_is_successful(self): 20 | self.assertEqual(self.response.status_code, 200) 21 | -------------------------------------------------------------------------------- /docker_tutorial/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = patterns('', 7 | url(r'^$', views.testpage, name='tutorial_testpage'), 8 | url(r'^api/$', views.api, name='tutorial_api'), 9 | url(r'^api/dockerfile_event/$', views.dockerfile_event, name='dockerfile_event'), 10 | url(r'^api/subscribe/$', views.subscribe, name='subscribe'), 11 | url(r'^api/metrics/$', views.get_metrics, name='get_metrics'), 12 | url(r'^stats/$', views.stats, name='stats'), 13 | ) 14 | -------------------------------------------------------------------------------- /docker_tutorial/utils.py: -------------------------------------------------------------------------------- 1 | from django.core.cache import cache 2 | from django.core.exceptions import ObjectDoesNotExist 3 | 4 | from docker_tutorial.models import TutorialUser 5 | 6 | 7 | def get_user_for_request(request): 8 | """ 9 | Function checks for an existing session, and creates one if necessary 10 | then checks for existing tutorialUser, and creates one if necessary 11 | returns this tutorialUser 12 | """ 13 | session_key = request.session._get_or_create_session_key() 14 | return _get_or_create_user(request, session_key) 15 | 16 | 17 | def _get_or_create_user(request, session_key): 18 | """ 19 | Fetches a user if one exists, creates one otherwise. 20 | """ 21 | cache_prefix = "_get_or_create_user:" 22 | cache_key = cache_prefix + session_key 23 | 24 | user = cache.get(cache_key) 25 | if user is None: 26 | try: 27 | user = TutorialUser.objects.filter( 28 | session_key=session_key).latest('pk') 29 | except ObjectDoesNotExist: 30 | user = TutorialUser.objects.create( 31 | session_key=session_key, 32 | 33 | # store some metadata about the user 34 | http_user_agent=request.META.get('HTTP_USER_AGENT', ''), 35 | http_remote_address=request.META.get('REMOTE_ADDR', ''), 36 | http_real_remote_address=request.META.get( 37 | 'HTTP_X_FORWARDED_FOR', ''), 38 | http_accept_language=request.META.get( 39 | 'HTTP_ACCEPT_LANGUAGE', ''), 40 | http_referrer=request.META.get('HTTP_REFERER', '') 41 | ) 42 | finally: 43 | cache.set(cache_key, user) 44 | 45 | return user -------------------------------------------------------------------------------- /docker_tutorial/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.shortcuts import render_to_response 4 | from django.template import RequestContext 5 | from django.views.decorators.csrf import csrf_exempt 6 | from django.http import HttpResponse 7 | 8 | from . import utils 9 | from .models import TutorialUser, TutorialEvent, DockerfileEvent, Subscriber 10 | 11 | __author__ = 'thatcher' 12 | 13 | 14 | def endpoint(request): 15 | """ 16 | api endpoint for saving events 17 | """ 18 | 19 | return render_to_response("base/home.html", { 20 | }, context_instance=RequestContext(request)) 21 | 22 | 23 | def testpage(request): 24 | """ 25 | testing saving stuff on a users' session 26 | """ 27 | 28 | username = request.session.get('username', 'unknown') 29 | print username 30 | 31 | return render_to_response("tutorial/testpage.html", { 32 | # "form": form, 33 | 'username': username 34 | }, context_instance=RequestContext(request)) 35 | 36 | 37 | @csrf_exempt 38 | def api(request): 39 | """ 40 | saving the user's events 41 | """ 42 | if request.method == "POST": 43 | user = utils.get_user_for_request(request) 44 | 45 | event = TutorialEvent.objects.create( 46 | user=user, 47 | type=request.POST.get('type', TutorialEvent.NONE), 48 | question=request.POST.get('question', None), 49 | command=request.POST.get('command', "")[:79], 50 | feedback=request.POST.get('feedback', ""), 51 | ) 52 | 53 | return HttpResponse('.') 54 | 55 | 56 | def dockerfile_event(request): 57 | """ 58 | saving the dockerfile events 59 | """ 60 | 61 | if request.method == "POST": 62 | user = utils.get_user_for_request(request) 63 | event = DockerfileEvent.objects.create(user=user) 64 | # event.user ; is set at instantiation 65 | ## default type is 'none' 66 | event.errors = request.POST.get('errors', None) 67 | event.level = request.POST.get('level', None) 68 | event.item = request.POST.get('item', 'none') 69 | # event.timestamp ; is automatically set 70 | event.save() 71 | 72 | return HttpResponse('0') 73 | 74 | 75 | def subscribe(request): 76 | """ 77 | Save the users' email. -- Mainly / only meant for notifying them of the availability of 78 | a next tutorial 79 | """ 80 | if request.POST: 81 | user = utils.get_user_for_request(request) 82 | email = request.POST.get('email', None) 83 | from_level = request.POST.get('from_level', None) 84 | 85 | try: 86 | subscriber = Subscriber.objects.create( 87 | user=user, email=email, from_level=from_level) 88 | Subscriber.save(subscriber) 89 | return HttpResponse('0', status=200) 90 | except: 91 | return HttpResponse('1', status=200) 92 | else: 93 | return HttpResponse('1', status=200) 94 | 95 | 96 | def stats(request): 97 | 98 | 99 | users = {} 100 | users['started'] = TutorialUser.objects.all().count() 101 | users['completed'] = TutorialEvent.objects.values_list('user', 'type').filter(type='complete').distinct().count() 102 | 103 | i = 0 104 | answered = {} 105 | while i < 9: 106 | number = TutorialEvent.objects.values_list('user', 'type', 'question').filter(type='next', question=i).distinct().count() 107 | answered[i] = number 108 | i = i + 1 109 | 110 | # Append the completed, because question 9 has no 'next' 111 | answered[9] = users['completed'] 112 | 113 | 114 | # find which question people look for the answer 115 | i = 0 116 | peeks = {} 117 | while i < 10: 118 | number = TutorialEvent.objects.values_list('user', 'type', 'question').filter(type='peek', question=i).distinct().count() 119 | peeks[i] = number 120 | i = i + 1 121 | 122 | outputformat = request.GET.get("format", None) 123 | if outputformat: 124 | response = {} 125 | response['users'] = users 126 | response['peeks'] = peeks 127 | response['answered'] = answered 128 | jsonresponse = json.dumps(response) 129 | return HttpResponse(jsonresponse, mimetype="application/json") 130 | 131 | return render_to_response("tutorial/stats.html", { 132 | 'users': users, 133 | 'peeks': peeks, 134 | 'answered': answered 135 | }, context_instance=RequestContext(request)) 136 | 137 | 138 | def get_metrics(request): 139 | 140 | users = {} 141 | 142 | # TutorialUsers = TutorialUser.objects.all() 143 | # users['started_interactive'] = TutorialUsers.filter(TutorialEvent__count > 0) 144 | # users['completed'] = TutorialEvent.objects.filter(type='complete').count() 145 | 146 | interactive_start_count = TutorialEvent.objects.values_list('user').distinct().count() 147 | interactive_complete_count = TutorialEvent.objects.values_list('user','type').filter(type=TutorialEvent.COMPLETE).distinct().count() 148 | 149 | dockerfile_tutorial_count = DockerfileEvent.objects.values_list('user').distinct().count() 150 | dockerfile_tutorial_complete_level1 = DockerfileEvent.objects.values_list('user', 'level', 'errors').filter(errors=0).distinct().count() 151 | 152 | response = [] 153 | response.append({'name': 'tutorial_interactive_started', 'value_i': interactive_start_count}) 154 | response.append({'name': 'tutorial_interactive_completed', 'value_i': interactive_complete_count}) 155 | response.append({'name': 'dockerfile_tutorial_total', 'value_i': dockerfile_tutorial_count}) 156 | response.append({'name': 'tutorial_dockerfile_completed_level1', 'value_i': dockerfile_tutorial_complete_level1}) 157 | 158 | jsonresponse = json.dumps(response) 159 | 160 | return HttpResponse(jsonresponse, mimetype="application/json") 161 | -------------------------------------------------------------------------------- /dockerfile_tutorial/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'thatcher' 2 | -------------------------------------------------------------------------------- /dockerfile_tutorial/static/css/dockerfile_tutorial.css: -------------------------------------------------------------------------------- 1 | pre { 2 | background-color: #F5F5F5; 3 | border: 1px solid rgba(0, 0, 0, 0.15); 4 | border-radius: 4px 4px 4px 4px; 5 | display: block; 6 | font-size: 13px; 7 | line-height: 20px; 8 | margin: 0 0 10px; 9 | padding: 9.5px; 10 | white-space: pre-wrap; 11 | word-break: break-all; 12 | word-wrap: break-word; 13 | } 14 | code, pre { 15 | border-radius: 3px 3px 3px 3px; 16 | color: #333333; 17 | font-family: Monaco,Menlo,Consolas,"Courier New",monospace; 18 | font-size: 12px; 19 | padding: 0 3px 2px; 20 | } 21 | 22 | .terminal { 23 | color: #AAAAAA; 24 | background-color: black; 25 | } 26 | 27 | .terminal span.command { 28 | color: white; 29 | font-weight: bold; 30 | } 31 | 32 | .error_input { 33 | border-color: #B94A48 !important; 34 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset !important; 35 | } -------------------------------------------------------------------------------- /dockerfile_tutorial/static/js/dockerfile_tutorial.js: -------------------------------------------------------------------------------- 1 | if (!String.prototype.trim) 2 | { 3 | String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ''); }; 4 | } 5 | 6 | function clean_input(i) 7 | { 8 | return (i.trim()); 9 | } 10 | 11 | function dockerfile_log(level, item, errors) 12 | { 13 | var logUrl = '/tutorial/api/dockerfile_event/'; 14 | $.ajax({ 15 | url: logUrl, 16 | type: "POST", 17 | cache:false, 18 | data: { 19 | 'errors': errors, 20 | 'level': level, 21 | 'item': item, 22 | }, 23 | }).done( function() { } ); 24 | } 25 | 26 | function validate_email(email) 27 | { 28 | var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 29 | return re.test(email); 30 | } 31 | 32 | $(document).ready(function() { 33 | 34 | /* prepare to send the csrf-token on each ajax-request */ 35 | var csrftoken = $.cookie('csrftoken'); 36 | $.ajaxSetup({ 37 | headers: { 'X-CSRFToken': csrftoken } 38 | }); 39 | 40 | $("#send_email").click( function() 41 | { 42 | $('#email_invalid').hide(); 43 | $('#email_already_registered').hide(); 44 | $('#email_registered').hide(); 45 | 46 | email = $('#email').val(); 47 | if (!validate_email(email)) 48 | { 49 | $('#email_invalid').show(); 50 | return (false); 51 | } 52 | 53 | var emailUrl = '/tutorial/api/subscribe/'; 54 | 55 | $.ajax({ 56 | url: emailUrl, 57 | type: "POST", 58 | cache:false, 59 | data: { 60 | 'email': email, 61 | 'from_level': $(this).data('level') 62 | }, 63 | }).done( function(data ) { 64 | if (data == 1) // already registered 65 | { 66 | $('#email_already_registered').show(); 67 | } 68 | else if (data == 0) // registered ok 69 | { 70 | $('#email_registered').show(); 71 | } 72 | 73 | } ); 74 | return (true); 75 | }); 76 | }) 77 | -------------------------------------------------------------------------------- /dockerfile_tutorial/static/js/dockerfile_tutorial_level1.js: -------------------------------------------------------------------------------- 1 | function check_form () 2 | { 3 | $('#level1_error0').hide(); 4 | $('#level1_error1').hide(); 5 | $('#level1_error2').hide(); 6 | $('#level1_error3').hide(); 7 | 8 | $('#no_good').hide(); 9 | $('#some_good').hide(); 10 | $('#all_good').hide(); 11 | 12 | var a = clean_input($('#level1_q0').val()).toUpperCase(); 13 | var b = clean_input($('#level1_q1').val()).toUpperCase(); 14 | var c = clean_input($('#level1_q2').val()).toUpperCase(); 15 | var d = clean_input($('#level1_q3').val()); 16 | var points = 0; 17 | 18 | if (a == 'FROM') 19 | { 20 | points = points + 1; 21 | } 22 | else 23 | { 24 | $('#level1_error0').show(); 25 | } 26 | if (b == 'RUN') 27 | { 28 | points = points + 1; 29 | } 30 | else 31 | { 32 | $('#level1_error1').show(); 33 | } 34 | if (c == 'MAINTAINER') 35 | { 36 | points = points + 1; 37 | } 38 | else 39 | { 40 | $('#level1_error2').show(); 41 | } 42 | if (d == '#') 43 | { 44 | points = points + 1; 45 | } 46 | else 47 | { 48 | $('#level1_error3').show(); 49 | } 50 | if (points == 4) // all good 51 | { 52 | $('#all_good').show(); 53 | } 54 | else if (points == 0) // nothing good 55 | { 56 | $('#no_good').show(); 57 | } 58 | else // some good some bad 59 | { 60 | $('#some_good').show(); 61 | } 62 | return (4 - points); 63 | } 64 | 65 | 66 | 67 | function check_fill() 68 | { 69 | 70 | $('#dockerfile_ok').hide(); 71 | $('#dockerfile_ko').hide(); 72 | 73 | var from = clean_input($('#from').val()).toUpperCase(); 74 | var ubuntu = clean_input($('#ubuntu').val()).toUpperCase(); 75 | var maintainer = clean_input($('#maintainer').val()).toUpperCase(); 76 | var eric = clean_input($('#eric').val()).toUpperCase(); 77 | var bardin = clean_input($('#bardin').val()).toUpperCase(); 78 | var run0 = clean_input($('#run0').val()).toUpperCase(); 79 | var run1 = clean_input($('#run1').val()).toUpperCase(); 80 | var memcached = clean_input($('#memcached').val()).toUpperCase(); 81 | var run2 = clean_input($('#run2').val()).toUpperCase(); 82 | 83 | $('#from').attr("class", "input-small"); 84 | $('#ubuntu').attr("class", "input-small"); 85 | $('#maintainer').attr("class", "input-small"); 86 | $('#eric').attr("class", "input-small"); 87 | $('#bardin').attr("class", "input-small"); 88 | $('#run0').attr("class", "input-small"); 89 | $('#run1').attr("class", "input-small"); 90 | $('#memcached').attr("class", "input-small"); 91 | $('#run2').attr("class", "input-small"); 92 | 93 | var errors = 0; 94 | 95 | if (from != "FROM") 96 | { 97 | $('#from').attr("class", "input-small error_input"); 98 | errors = errors + 1; 99 | } 100 | if (ubuntu != "UNTU") 101 | { 102 | $('#ubuntu').attr("class", "input-small error_input"); 103 | errors = errors + 1; 104 | } 105 | if (maintainer != "MAINTAINER") 106 | { 107 | $('#maintainer').attr("class", "input-small error_input"); 108 | errors = errors + 1; 109 | } 110 | if (eric != "RIC") 111 | { 112 | $('#eric').attr("class", "input-small error_input"); 113 | errors = errors + 1; 114 | } 115 | if (bardin != "ARDIN") 116 | { 117 | $('#bardin').attr("class", "input-small error_input"); 118 | errors = errors + 1; 119 | } 120 | if (run0 != "RUN") 121 | { 122 | $('#run0').attr("class", "input-small error_input"); 123 | errors = errors + 1; 124 | } 125 | if (run1 != "RUN") 126 | { 127 | $('#run1').attr("class", "input-small error_input"); 128 | errors = errors + 1; 129 | } 130 | if (run2 != "RUN") 131 | { 132 | $('#run2').attr("class", "input-small error_input"); 133 | errors = errors + 1; 134 | } 135 | if (memcached != "MEMCACHED") 136 | { 137 | $('#memcached').attr("class", "input-small error_input"); 138 | errors = errors + 1; 139 | } 140 | 141 | if (errors != 0) 142 | { 143 | $('#dockerfile_ko').show(); 144 | } 145 | else 146 | { 147 | $('#dockerfile_ok').show(); 148 | } 149 | return (errors); 150 | } 151 | 152 | $(document).ready(function() { 153 | 154 | $("#check_level1_questions").click( function() 155 | { 156 | errors = check_form(); 157 | dockerfile_log(1, '1_questions', errors); 158 | } 159 | ); 160 | 161 | $("#check_level1_fill").click( function() 162 | { 163 | var errors = check_fill(); 164 | dockerfile_log(1, '2_fill', errors); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /dockerfile_tutorial/static/js/dockerfile_tutorial_level2.js: -------------------------------------------------------------------------------- 1 | function check_form () 2 | { 3 | $('#level2_error0').hide(); 4 | $('#level2_error1').hide(); 5 | $('#level2_error2').hide(); 6 | $('#level2_error3').hide(); 7 | $('#level2_error4').hide(); 8 | $('#level2_error5').hide(); 9 | $('#level2_error6').hide(); 10 | $('#level2_error7').hide(); 11 | 12 | $('#no_good').hide(); 13 | $('#some_good').hide(); 14 | $('#all_good').hide(); 15 | 16 | var a = clean_input($('#level2_q0').val()).toUpperCase(); 17 | var b = clean_input($('#level2_q1').val()).toUpperCase(); 18 | var c = clean_input($('#level2_q2').val()).toUpperCase(); 19 | var d = clean_input($('#level2_q3').val()); 20 | var e = clean_input($('#level2_q4').val()).toUpperCase(); 21 | var f = clean_input($('#level2_q5').val()).toUpperCase(); 22 | var g = clean_input($('#level2_q6').val()).toUpperCase(); 23 | var h = clean_input($('#level2_q7').val()).toUpperCase(); 24 | var points = 0; 25 | 26 | if (a == 'FROM') 27 | { 28 | points = points + 1; 29 | } 30 | else 31 | { 32 | $('#level2_error0').show(); 33 | } 34 | if (b == 'RUN') 35 | { 36 | points = points + 1; 37 | } 38 | else 39 | { 40 | $('#level2_error1').show(); 41 | } 42 | if (c == 'MAINTAINER') 43 | { 44 | points = points + 1; 45 | } 46 | else 47 | { 48 | $('#level2_error2').show(); 49 | } 50 | if (d == '#') 51 | { 52 | points = points + 1; 53 | } 54 | else 55 | { 56 | $('#level2_error3').show(); 57 | } 58 | 59 | if (e == 'ENTRYPOINT' || e == 'CMD') 60 | { 61 | points = points + 1; 62 | } 63 | else 64 | { 65 | $('#level2_error4').show(); 66 | } 67 | 68 | if (f == 'USER') 69 | { 70 | points = points + 1; 71 | } 72 | else 73 | { 74 | $('#level2_error5').show(); 75 | } 76 | 77 | if (g == 'EXPOSE') 78 | { 79 | points = points + 1; 80 | } 81 | else 82 | { 83 | $('#level2_error6').show(); 84 | } 85 | if (h == 'ENTRYPOINT' || h == 'CMD') 86 | { 87 | points = points + 1; 88 | } 89 | else 90 | { 91 | $('#level2_error7').show(); 92 | } 93 | 94 | 95 | if (points == 8) // all good 96 | { 97 | $('#all_good').show(); 98 | } 99 | else if (points == 0) // nothing good 100 | { 101 | $('#no_good').show(); 102 | } 103 | else // some good some bad 104 | { 105 | $('#some_good').show(); 106 | } 107 | return (8- points); 108 | } 109 | 110 | function check_fill() 111 | { 112 | $('#dockerfile_ok').hide(); 113 | $('#dockerfile_ko').hide(); 114 | 115 | var from = clean_input($('#from').val()).toUpperCase(); 116 | var ubuntu = clean_input($('#ubuntu').val()).toUpperCase(); 117 | var maintainer = clean_input($('#maintainer').val()).toUpperCase(); 118 | var eric = clean_input($('#eric').val()).toUpperCase(); 119 | var bardin = clean_input($('#bardin').val()).toUpperCase(); 120 | var run0 = clean_input($('#run0').val()).toUpperCase(); 121 | var run1 = clean_input($('#run1').val()).toUpperCase(); 122 | var run2 = clean_input($('#run2').val()).toUpperCase(); 123 | var run3 = clean_input($('#run3').val()).toUpperCase(); 124 | var run4 = clean_input($('#run4').val()).toUpperCase(); 125 | var run5 = clean_input($('#run5').val()).toUpperCase(); 126 | var run6 = clean_input($('#run6').val()).toUpperCase(); 127 | var entrypoint = clean_input($('#entrypoint').val()).toUpperCase(); 128 | var user = clean_input($('#user').val()).toUpperCase(); 129 | var expose = clean_input($('#expose').val()).toUpperCase(); 130 | var gcc = clean_input($('#gcc').val()).toUpperCase(); 131 | 132 | $('#from').attr("class", "input-small"); 133 | $('#ubuntu').attr("class", "input-small"); 134 | $('#maintainer').attr("class", "input-small"); 135 | $('#eric').attr("class", "input-small"); 136 | $('#bardin').attr("class", "input-small"); 137 | $('#run0').attr("class", "input-small"); 138 | $('#run1').attr("class", "input-small"); 139 | $('#run2').attr("class", "input-small"); 140 | $('#run3').attr("class", "input-small"); 141 | $('#run4').attr("class", "input-small"); 142 | $('#run5').attr("class", "input-small"); 143 | $('#run6').attr("class", "input-small"); 144 | 145 | $('#entrypoint').attr("class", "input-small"); 146 | $('#user').attr("class", "input-small"); 147 | $('#expose').attr("class", "input-small"); 148 | $('#gcc').attr("class", "input-small"); 149 | 150 | var errors = 0; 151 | 152 | if (from != "FROM") 153 | { 154 | $('#from').attr("class", "input-small error_input"); 155 | errors = errors + 1; 156 | } 157 | if (ubuntu != "UNTU") 158 | { 159 | $('#ubuntu').attr("class", "input-small error_input"); 160 | errors = errors + 1; 161 | } 162 | if (maintainer != "AINER") 163 | { 164 | $('#maintainer').attr("class", "input-small error_input"); 165 | errors = errors + 1; 166 | } 167 | if (eric != "BERTO") 168 | { 169 | $('#eric').attr("class", "input-small error_input"); 170 | errors = errors + 1; 171 | } 172 | if (bardin != "SHIOKA") 173 | { 174 | $('#bardin').attr("class", "input-small error_input"); 175 | errors = errors + 1; 176 | } 177 | if (run0 != "RUN") 178 | { 179 | $('#run0').attr("class", "input-small error_input"); 180 | errors = errors + 1; 181 | } 182 | if (run1 != "RUN") 183 | { 184 | $('#run1').attr("class", "input-small error_input"); 185 | errors = errors + 1; 186 | } 187 | if (run2 != "RUN") 188 | { 189 | $('#run2').attr("class", "input-small error_input"); 190 | errors = errors + 1; 191 | } 192 | if (run3 != "RUN") 193 | { 194 | $('#run3').attr("class", "input-small error_input"); 195 | errors = errors + 1; 196 | } 197 | if (run4 != "RUN") 198 | { 199 | $('#run4').attr("class", "input-small error_input"); 200 | errors = errors + 1; 201 | } 202 | if (run5 != "RUN") 203 | { 204 | $('#run5').attr("class", "input-small error_input"); 205 | errors = errors + 1; 206 | } 207 | if (run6 != "RUN") 208 | { 209 | $('#run6').attr("class", "input-small error_input"); 210 | errors = errors + 1; 211 | } 212 | 213 | if (gcc != "GCC") 214 | { 215 | $('#gcc').attr("class", "input-small error_input"); 216 | errors = errors + 1; 217 | } 218 | if (entrypoint != "ENTRYPOINT") 219 | { 220 | $('#entrypoint').attr("class", "input-small error_input"); 221 | errors = errors + 1; 222 | } 223 | if (user != "USER") 224 | { 225 | $('#user').attr("class", "input-small error_input"); 226 | errors = errors + 1; 227 | } 228 | if (expose != "EXPOSE") 229 | { 230 | $('#expose').attr("class", "input-small error_input"); 231 | errors = errors + 1; 232 | } 233 | 234 | if (errors != 0) 235 | { 236 | $('#dockerfile_ko').show(); 237 | } 238 | else 239 | { 240 | $('#dockerfile_ok').show(); 241 | } 242 | return (errors); 243 | } 244 | 245 | $(document).ready(function() { 246 | 247 | $("#check_level2_questions").click( function() 248 | { 249 | errors = check_form(); 250 | dockerfile_log(2, '1_questions', errors); 251 | } 252 | ); 253 | 254 | $("#check_level2_fill").click( function() 255 | { 256 | var errors = check_fill(); 257 | dockerfile_log(2, '2_fill', errors); 258 | }); 259 | }); 260 | -------------------------------------------------------------------------------- /dockerfile_tutorial/static/js/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin v1.3.1 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2013 Klaus Hartl 6 | * Released under the MIT license 7 | */ 8 | (function ($, document, undefined) { 9 | 10 | var pluses = /\+/g; 11 | 12 | function raw(s) { 13 | return s; 14 | } 15 | 16 | function decoded(s) { 17 | return unRfc2068(decodeURIComponent(s.replace(pluses, ' '))); 18 | } 19 | 20 | function unRfc2068(value) { 21 | if (value.indexOf('"') === 0) { 22 | // This is a quoted cookie as according to RFC2068, unescape 23 | value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); 24 | } 25 | return value; 26 | } 27 | 28 | function fromJSON(value) { 29 | return config.json ? JSON.parse(value) : value; 30 | } 31 | 32 | var config = $.cookie = function (key, value, options) { 33 | 34 | // write 35 | if (value !== undefined) { 36 | options = $.extend({}, config.defaults, options); 37 | 38 | if (value === null) { 39 | options.expires = -1; 40 | } 41 | 42 | if (typeof options.expires === 'number') { 43 | var days = options.expires, t = options.expires = new Date(); 44 | t.setDate(t.getDate() + days); 45 | } 46 | 47 | value = config.json ? JSON.stringify(value) : String(value); 48 | 49 | return (document.cookie = [ 50 | encodeURIComponent(key), '=', config.raw ? value : encodeURIComponent(value), 51 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 52 | options.path ? '; path=' + options.path : '', 53 | options.domain ? '; domain=' + options.domain : '', 54 | options.secure ? '; secure' : '' 55 | ].join('')); 56 | } 57 | 58 | // read 59 | var decode = config.raw ? raw : decoded; 60 | var cookies = document.cookie.split('; '); 61 | var result = key ? null : {}; 62 | for (var i = 0, l = cookies.length; i < l; i++) { 63 | var parts = cookies[i].split('='); 64 | var name = decode(parts.shift()); 65 | var cookie = decode(parts.join('=')); 66 | 67 | if (key && key === name) { 68 | result = fromJSON(cookie); 69 | break; 70 | } 71 | 72 | if (!key) { 73 | result[name] = fromJSON(cookie); 74 | } 75 | } 76 | 77 | return result; 78 | }; 79 | 80 | config.defaults = {}; 81 | 82 | $.removeCookie = function (key, options) { 83 | if ($.cookie(key) !== null) { 84 | $.cookie(key, null, options); 85 | return true; 86 | } 87 | return false; 88 | }; 89 | 90 | })(jQuery, document); 91 | -------------------------------------------------------------------------------- /dockerfile_tutorial/templates/dockerfile/_dockerfile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_styles %} 4 | 5 | {% endblock page_styles %} 6 | {% block page_js %} 7 | 8 | 9 | {% endblock page_js %} 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /dockerfile_tutorial/templates/dockerfile/introduction.html: -------------------------------------------------------------------------------- 1 | {% extends 'dockerfile/_dockerfile.html' %} 2 | {% load staticfiles %}{% load google_analytics %}{% load navactive %} 3 | 4 | {% block content %} 5 | 6 |
7 |
8 |
9 |
10 |

Dockerfile tutorial

11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 | 19 |

Level 0 - Introduction

20 |

21 | As we showed in the introductory tutorial, containers can be built manually.
22 | However, with a Dockerfile, you can describe build steps once and then build a container automatically--any time you want--from source. 23 |

24 |

25 | Dockerfiles can be viewed as an image representation. They provide a simple syntax for building images and they are a great way to automate and script the images creation. 26 | If you are really serious about Docker, you should master the Dockerfile syntax. Let’s start! 27 |

28 |

29 | If you haven't taken the introductory tutorial, you should do so before starting this.
30 |
31 |

32 | 35 |
36 |
37 |
38 |
39 | 40 | 41 | 42 | {% endblock %} -------------------------------------------------------------------------------- /dockerfile_tutorial/templates/dockerfile/level1.html: -------------------------------------------------------------------------------- 1 | {% extends 'dockerfile/_dockerfile.html' %} 2 | {% load staticfiles %}{% load google_analytics %}{% load navactive %} 3 | 4 | {% block page_js %} 5 | {{ block.super }} 6 | 7 | {% endblock page_js %} 8 | 9 | 10 | {% block content %} 11 | 12 | 13 |
14 |
15 |
16 |
17 |

Dockerfile tutorial

18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 |

Level 1 - FROM, RUN, #, MAINTAINER

27 | 28 |

29 | In this section we will cover the most important instructions: FROM and RUN. 30 | We will also see how to use the docker build command and how to specify the maintainer of the Dockerfile and how to insert comments. 31 |

32 | 33 |

Dockerfile instructions

34 |

35 | All Dockerfile instructions look like this: 36 |

37 | 38 | 39 |
INSTRUCTION arguments
40 | 41 | 42 |

43 | Instructions are not case sensitive, but we recommend to use capital letters. 44 |

45 | 46 |

The FROM instruction

47 | 48 |

49 | The first instruction of every Dockerfile needs to be the FROM instruction 50 |

51 | 52 |
FROM image
53 | 54 |

55 | It sets the base image for subsequent instructions.
56 | Example: 57 |

58 |
FROM ubuntu
59 |

60 | In this example, we chose ubuntu as our base image. ubuntu is a “General use Ubuntu base image” provided by the Docker team. 61 | You can find a list of available images from the command line by using the docker search command.
62 | Example: 63 |

64 |
 65 | root@precise64:/home/vagrant/dockerfiles# docker search centos
 66 | Found 14 results matching your query ("centos")
 67 | NAME                             DESCRIPTION
 68 | centos
 69 | backjlack/centos-6.4-x86_64
 70 | creack/centos
 71 | mbkan/lamp                       centos with ssh, LAMP, PHPMyAdmin(root password 'toor' and MySQL root password 'sysadmin'). Auto starts sshd, MySQL and Apache. Just launch us...
 72 | zenosslabs/centos
 73 | slantview/centos-chef-solo       CentOS 6.4 with chef-solo.
 74 | tchap/centos-epel                The official centos image having EPEL added among its repositories.
 75 | backjlack/centos                 This repository contains the following images: Centos 5.9 (more to be added soon)
 76 | oss17888/centos6.4
 77 | markl/centos
 78 | troygoode/centos-node-hello
 79 | austin/centos-base
 80 | estrai/centos32                  Centos 5.9 32-bit based on an OpenVZ image.
 81 | comodit/centos
 82 | 
83 |

84 | So let’s say you want to use centos as you base image, you can for instance use the following line:

85 |
 86 | FROM centos
 87 | 
88 |

89 | You can also browse available images from Docker and the community online: index.docker.io 90 |

91 |

The RUN instruction

92 |

93 | The RUN instruction will execute any commands on the current image and commit the results. The resulting committed image will be used for the next step in the Dockerfile.
94 | Layering RUN instructions and generating commits conforms to the core concepts of Docker where commits are cheap and containers can be created from any point in an image’s history, much like source control.

95 |
 96 | RUN command
 97 | 
98 |

99 | RUN command is equivalent to the docker commands: docker run image command + docker commit container_id, 100 | Where image would be replaced automatically with the current image, and container_id would be the result of the previous RUN instruction.
101 | Example:

102 |
103 | RUN apt-get install -y memcached
104 | 
105 |

106 | As we saw earlier, we need to have the FROM instruction before any RUN instruction, so that docker knows from which base image to start running the instruction on. For instance: 107 |

108 |
109 | FROM ubuntu
110 | RUN apt-get install -y memcached
111 | 
112 |

113 | This example creates an image with memcached installed on the ubuntu image. It is the equivalent to the following command lines: 114 |

115 |
116 | root@precise64:/home/vagrant/dockerfiles# docker run ubuntu apt-get install -y memcached
117 | […]
118 | [=> container id is 9fb69e798e67]
119 | root@precise64:/home/vagrant/dockerfiles# docker commit 9fb69e798e67
120 | 6742949a1bae
121 | 
122 | 123 |

docker build

124 |

125 | Once you have your Dockerfile ready, you can use the docker build command to create your image from it. There are different ways to use this command: 126 |

    127 |
  1. 128 | If your Dockerfile is in your current directory you can use docker build . 129 |
  2. 130 |
  3. 131 | From stdin. For instance: docker build - < Dockerfile 132 |
  4. 133 |
  5. 134 | From GitHub. For instance: docker build github.com/creack/docker-firefox
    135 | In this last example, docker will clone the GitHub repository and use it as context. The Dockerfile at the root of the repository will be used to create the image. 136 |
  6. 137 |
138 |

139 | Let’s run our last Dockerfile example. 140 |

141 |
142 | root@precise64:/home/vagrant/dockerfiles# cat Dockerfile
143 | FROM ubuntu
144 | RUN apt-get install -y memcached
145 | root@precise64:/home/vagrant/dockerfiles# docker build .
146 | Uploading context 20480 bytes
147 | Step 1 : FROM ubuntu
148 | Pulling repository ubuntu
149 | Pulling image 8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c (precise) from ubuntu
150 | Pulling image b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc (quantal) from ubuntu
151 | Pulling b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc metadata
152 | Pulling b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc fs layer
153 | Downloading 10.24 kB/10.24 kB (100%)
154 | Pulling 27cf784147099545 metadata
155 | Pulling 27cf784147099545 fs layer
156 | Downloading 10.24 kB/10.24 kB (100%)
157 | Pulling 27cf784147099545 metadata
158 | Pulling 27cf784147099545 fs layer
159 | Downloading 94.86 MB/94.86 MB (100%)
160 |  ---> 8dbd9e392a96
161 | Step 2 : RUN apt-get install -y memcached
162 |  ---> Running in dedc7af62d9f
163 | Reading package lists...
164 | Building dependency tree...
165 | The following extra packages will be installed:
166 |   libclass-isa-perl libevent-2.0-5 libgdbm3 libswitch-perl netbase perl
167 |   perl-modules
168 | Suggested packages:
169 |   libcache-memcached-perl libmemcached perl-doc libterm-readline-gnu-perl
170 |   libterm-readline-perl-perl make libpod-plainer-perl
171 | The following NEW packages will be installed:
172 |   libclass-isa-perl libevent-2.0-5 libgdbm3 libswitch-perl memcached netbase
173 |   perl perl-modules
174 | 0 upgraded, 8 newly installed, 0 to remove and 0 not upgraded.
175 | Need to get 8070 kB of archives.
176 | After this operation, 32.8 MB of additional disk space will be used.
177 | Get:1 http://archive.ubuntu.com/ubuntu/ precise/main libgdbm3 amd64 1.8.3-10 [35.3 kB]
178 | Get:2 http://archive.ubuntu.com/ubuntu/ precise/main netbase all 4.47ubuntu1 [15.0 kB]
179 | Get:3 http://archive.ubuntu.com/ubuntu/ precise/main libclass-isa-perl all 0.36-3 [11.9 kB]
180 | Get:4 http://archive.ubuntu.com/ubuntu/ precise/main libevent-2.0-5 amd64 2.0.16-stable-1 [127 kB]
181 | Get:5 http://archive.ubuntu.com/ubuntu/ precise/main perl-modules all 5.14.2-6ubuntu2 [3369 kB]
182 | Get:6 http://archive.ubuntu.com/ubuntu/ precise/main perl amd64 5.14.2-6ubuntu2 [4417 kB]
183 | Get:7 http://archive.ubuntu.com/ubuntu/ precise/main libswitch-perl all 2.16-2 [19.2 kB]
184 | Get:8 http://archive.ubuntu.com/ubuntu/ precise/main memcached amd64 1.4.13-0ubuntu2 [75.3 kB]
185 | debconf: delaying package configuration, since apt-utils is not installed
186 | Fetched 8070 kB in 7s (1136 kB/s)
187 | Selecting previously unselected package libgdbm3.
188 | (Reading database ... 7545 files and directories currently installed.)
189 | Unpacking libgdbm3 (from .../libgdbm3_1.8.3-10_amd64.deb) ...
190 | Selecting previously unselected package netbase.
191 | Unpacking netbase (from .../netbase_4.47ubuntu1_all.deb) ...
192 | Selecting previously unselected package libclass-isa-perl.
193 | Unpacking libclass-isa-perl (from .../libclass-isa-perl_0.36-3_all.deb) ...
194 | Selecting previously unselected package libevent-2.0-5.
195 | Unpacking libevent-2.0-5 (from .../libevent-2.0-5_2.0.16-stable-1_amd64.deb) ...
196 | Selecting previously unselected package perl-modules.
197 | Unpacking perl-modules (from .../perl-modules_5.14.2-6ubuntu2_all.deb) ...
198 | Selecting previously unselected package perl.
199 | Unpacking perl (from .../perl_5.14.2-6ubuntu2_amd64.deb) ...
200 | Selecting previously unselected package libswitch-perl.
201 | Unpacking libswitch-perl (from .../libswitch-perl_2.16-2_all.deb) ...
202 | Selecting previously unselected package memcached.
203 | Unpacking memcached (from .../memcached_1.4.13-0ubuntu2_amd64.deb) ...
204 | Setting up libgdbm3 (1.8.3-10) ...
205 | Setting up netbase (4.47ubuntu1) ...
206 | Setting up libclass-isa-perl (0.36-3) ...
207 | Setting up libevent-2.0-5 (2.0.16-stable-1) ...
208 | Setting up perl-modules (5.14.2-6ubuntu2) ...
209 | Setting up perl (5.14.2-6ubuntu2) ...
210 | update-alternatives: using /usr/bin/prename to provide /usr/bin/rename (rename) in auto mode.
211 | Setting up memcached (1.4.13-0ubuntu2) ...
212 | Starting memcached: memcached.
213 | Setting up libswitch-perl (2.16-2) ...
214 | Processing triggers for libc-bin ...
215 | ldconfig deferred processing now taking place
216 |  ---> 1dcfa24c8ca6
217 | Successfully built 1dcfa24c8ca6
218 | 
219 |

220 | You can see your newly created image 1dcfa24c8ca6 in the ouput of docker images: 221 |

222 |
223 | root@precise64:/home/vagrant/dockerfiles# docker images
224 | REPOSITORY          TAG                 ID                  CREATED              SIZE
225 | ubuntu              12.04               8dbd9e392a96        4 months ago         131.5 MB (virtual 131.5 MB)
226 | ubuntu              12.10               b750fe79269d        4 months ago         24.65 kB (virtual 180.1 MB)
227 | ubuntu              latest              8dbd9e392a96        4 months ago         131.5 MB (virtual 131.5 MB)
228 | ubuntu              precise             8dbd9e392a96        4 months ago         131.5 MB (virtual 131.5 MB)
229 | ubuntu              quantal             b750fe79269d        4 months ago         24.65 kB (virtual 180.1 MB)
230 | <none>              <none>               1275893fed44        9 minutes ago        12.3 kB (virtual 566.4 MB)
231 | <none>               <none>               1dcfa24c8ca6        About a minute ago   52.27 MB (virtual 183.8 MB)
232 | 
233 |

234 | Note that it does not have a name yet. Let’s name it with the command docker tag. 235 |

236 |
237 | root@precise64:/home/vagrant/dockerfiles# docker tag 1dcfa24c8ca6 memcached
238 | root@precise64:/home/vagrant/dockerfiles# docker images
239 | REPOSITORY          TAG                 ID                  CREATED             SIZE
240 | ubuntu              12.04               8dbd9e392a96        4 months ago        131.5 MB (virtual 131.5 MB)
241 | ubuntu              12.10               b750fe79269d        4 months ago        24.65 kB (virtual 180.1 MB)
242 | ubuntu              latest              8dbd9e392a96        4 months ago        131.5 MB (virtual 131.5 MB)
243 | ubuntu              precise             8dbd9e392a96        4 months ago        131.5 MB (virtual 131.5 MB)
244 | ubuntu              quantal             b750fe79269d        4 months ago        24.65 kB (virtual 180.1 MB)
245 | memcached           latest              1dcfa24c8ca6        4 minutes ago       52.27 MB (virtual 183.8 MB)
246 | <none>              <none>             1275893fed44        12 minutes ago      12.3 kB (virtual 566.4 MB)
247 | 
248 |

249 | The good news is that docker build comes with the option -t with which you can directly tag the (last) image created with the Dockerfile. So we could have used docker build -t memcached . directly. 250 |

251 |
252 | root@precise64:/home/vagrant/dockerfiles# docker build -t memcached_new .
253 | Uploading context 20480 bytes
254 | Step 1 : FROM ubuntu
255 | […]
256 | Step 2 : RUN apt-get install -y memcached
257 | […]
258 | Successfully built 1dcfa24c8ca6
259 | root@precise64:/home/vagrant/dockerfiles# docker images
260 | REPOSITORY          TAG                 ID                  CREATED             SIZE
261 | ubuntu              12.04               8dbd9e392a96        4 months ago        131.5 MB (virtual 131.5 MB)
262 | ubuntu              12.10               b750fe79269d        4 months ago        24.65 kB (virtual 180.1 MB)
263 | ubuntu              latest              8dbd9e392a96        4 months ago        131.5 MB (virtual 131.5 MB)
264 | ubuntu              precise             8dbd9e392a96        4 months ago        131.5 MB (virtual 131.5 MB)
265 | ubuntu              quantal             b750fe79269d        4 months ago        24.65 kB (virtual 180.1 MB)
266 | memcached_new       latest              1dcfa24c8ca6        4 minutes ago       52.27 MB (virtual 183.8 MB)
267 | <none>              <none>              1275893fed44        4 minutes ago      12.3 kB (virtual 566.4 MB)
268 | 
269 | 270 |

Creating a memcached image with a Dockerfile

271 |

272 | We have written almost all of it in the previous sections. 273 |

274 |
275 | FROM ubuntu
276 | RUN apt-get install -y memcached
277 | 
278 |

279 | But we want to ensure that the ubuntu package repository is up to date. With the command line we would do:

280 |
281 | root@precise64:/home/vagrant/dockerfiles# echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
282 | root@precise64:/home/vagrant/dockerfiles# apt-get update
283 | 
284 |

285 | Within the Dockerfile, we will use the RUN instruction. 286 |

287 |
288 | RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
289 | RUN apt-get update
290 | 
291 |

292 | So our final Dockerfile looks like this: 293 |

294 |
295 | FROM ubuntu
296 | RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
297 | RUN apt-get update
298 | RUN apt-get install -y memcached
299 | 
300 |

301 | Note that this Dockerfile would be equivalent to the following command lines:

302 |
303 | root@precise64:/home/vagrant/dockerfiles# docker run ubuntu echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
304 | […]
305 | [=> container id is 72d2a39bc64d]
306 | root@precise64:/home/vagrant/dockerfiles# docker commit 72d2a39bc64d
307 | d7a97544c0c7
308 | root@precise64:/home/vagrant/dockerfiles# docker run d7a97544c0c7 apt-get update
309 | […]
310 | [=> container id is 66b22a0ef7c9]
311 | root@precise64:/home/vagrant/dockerfiles# docker commit 66b22a0ef7c9
312 | a5f142a604b3
313 | root@precise64:/home/vagrant/dockerfiles# docker run a5f142a604b3 apt-get install -y memcached
314 | […]
315 | [=> container id is 9fb69e798e67]
316 | root@precise64:/home/vagrant/dockerfiles# docker commit 9fb69e798e67
317 | 6742949a1bae
318 | 
319 |

320 | Let’s create our image. We will use the -t option with docker build in order to give a name to our image. 321 |

322 |
323 | root@precise64:/home/vagrant/dockerfiles# cat Dockerfile
324 | FROM ubuntu
325 | RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
326 | RUN apt-get update
327 | RUN apt-get install -y memcached
328 | root@precise64:/home/vagrant/dockerfiles# docker build -t brand_new_memcached - < Dockerfile
329 | Uploading context 2048 bytes
330 | Step 1 : FROM ubuntu
331 |  ---> 8dbd9e392a96
332 | Step 2 : RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
333 |  ---> Using cache
334 |  ---> 913c2344b312
335 | Step 3 : RUN apt-get update
336 |  ---> Using cache
337 |  ---> 0a77b6e48393
338 | Step 4 : RUN apt-get install -y memcached
339 |  ---> Running in d1eadc19edc6
340 | Reading package lists...
341 | Building dependency tree...
342 | The following extra packages will be installed:
343 |   libclass-isa-perl libevent-2.0-5 libgdbm3 libswitch-perl netbase perl
344 |   perl-modules
345 | […]
346 | Starting memcached: memcached.
347 | Setting up libswitch-perl (2.16-2) ...
348 | Processing triggers for libc-bin ...
349 | ldconfig deferred processing now taking place
350 |  ---> 782a37534312
351 | Successfully built 782a37534312
352 | 
353 |

354 | The id of the image is 782a37534312 but it already has a name as we can verify with docker images: 355 |

356 |
357 | root@precise64:/home/vagrant/dockerfiles# docker images
358 | REPOSITORY            TAG                 ID                  CREATED             SIZE
359 | memcached_new         latest              1dcfa24c8ca6        12 minutes ago      52.27 MB (virtual 183.8 MB)
360 | brand_new_memcached   latest              782a37534312        54 seconds ago      74.7 MB (virtual 350.7 MB)
361 | ubuntu                12.04               8dbd9e392a96        4 months ago        131.5 MB (virtual 131.5 MB)
362 | ubuntu                12.10               b750fe79269d        4 months ago        24.65 kB (virtual 180.1 MB)
363 | ubuntu                latest              8dbd9e392a96        4 months ago        131.5 MB (virtual 131.5 MB)
364 | ubuntu                precise             8dbd9e392a96        4 months ago        131.5 MB (virtual 131.5 MB)
365 | ubuntu                quantal             b750fe79269d        4 months ago        24.65 kB (virtual 180.1 MB)
366 | memcached             latest              1dcfa24c8ca6        12 minutes ago      52.27 MB (virtual 183.8 MB)
367 | <none>                <none>             1275893fed44        21 minutes ago      12.3 kB (virtual 566.4 MB)
368 | 
369 |

370 | Congratulations! You just created your first image with a Dockerfile :)

371 |

372 | You can verify that memcached is installed in your image by running these commands:

373 |
374 | root@precise64:/home/vagrant/dockerfiles# docker run -i -t brand_new_memcached /bin/bash
375 | root@c28a2c4be825:/# memcached
376 | can't run as root without the -u switch
377 | 
378 | 379 |

Commenting

380 |

381 | Code should always be commented. The same rule applies to the Dockerfile. 382 | A character # at the beginning of a line defines a comment. The comments are not exectuted. Comments are not mandatory, but we recommend using them to: 383 |

    384 |
  • Give a title and/or describe the purpose of the Dockerfile
  • 385 |
  • Give the version of your Dockerfile
  • 386 |
  • Comment individual Dockerfile lines and instructions
  • 387 |
  • 388 |
389 | So the Dockerfile to create a memcached image becomes:

390 |
391 | # Memcached
392 | #
393 | # VERSION       1.0
394 | 
395 | # use the ubuntu base image provided by dotCloud
396 | FROM ubuntu
397 | 
398 | # make sure the package repository is up to date
399 | RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
400 | RUN apt-get update
401 | 
402 | # install memcached
403 | RUN apt-get install -y memcached
404 | 
405 | 406 |

The MAINTAINER instruction

407 |

408 | As you could expect, the MAINTAINER instruction is used to specify name and contact information of the maintainer of the Dockerfile.
409 | Example: 410 |

411 |
412 | MAINTAINER Guillaume J. Charmes, guillaume@dotcloud.com
413 | 
414 |

415 | The final version of the Dockerfile becomes

416 |
417 | # Memcached
418 | #
419 | # VERSION       1.0
420 | 
421 | # use the ubuntu base image provided by dotCloud
422 | FROM ubuntu
423 | MAINTAINER Guillaume J. Charmes, guillaume@dotcloud.com
424 | # make sure the package repository is up to date
425 | RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
426 | RUN apt-get update
427 | 
428 | # install memcached
429 | RUN apt-get install -y memcached
430 | 
431 | 432 |

Test your Dockerfile skills - Level 1

433 |

Questions

434 | 435 |
436 | What is the Dockerfile instruction to specify the base image ?
437 | 438 |
439 | What is the Dockerfile instruction to execute any commands on the current image and commit the results?
440 | 441 |
442 | What is the Dockerfile instruction to specify the maintainer of the Dockerfile?
443 | 444 |
445 | What is the character used to add comment in Dockerfiles?
446 | 447 |
448 | 452 | 453 | 454 | 455 |
456 | 457 | 458 |

Fill the Dockerfile

459 |

460 | Your best friend Eric Bardin sent you a Dockerfile, but some parts were lost in the ocean. Can you find the missing parts? 461 |

462 |
463 |
464 | # This is a Dockerfile to create an image with Memcached and Emacs installed.
465 | #
466 | # VERSION       1.0
467 | 
468 | # use the ubuntu base image provided by dotCloud
469 |  ub
470 | 
471 |  E B, eric.bardin@dotcloud.com
472 | 
473 | # make sure the package repository is up to date
474 |  echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
475 |  apt-get update
476 | 
477 | # install memcached
478 | RUN apt-get install -y 
479 | 
480 | # install emacs
481 |  apt-get install -y emacs23
482 | 
483 | 
484 |
485 | 486 | 489 | 490 |

491 | 492 |

What's next?

493 | 494 |

In the next level, we will go into more detail about how to specify which command should be executed when the container starts, 495 | which user to use, and how expose a particular port.

496 | 497 | Go to the next level 498 | 499 | 500 |
501 |
502 | 503 |
504 |
505 | 506 | 507 | 508 | {% endblock %} 509 | -------------------------------------------------------------------------------- /dockerfile_tutorial/templates/dockerfile/level2.html: -------------------------------------------------------------------------------- 1 | {% extends 'dockerfile/_dockerfile.html' %} 2 | {% load staticfiles %}{% load google_analytics %}{% load navactive %} 3 | 4 | {% block page_js %} 5 | {{ block.super }} 6 | 7 | {% endblock page_js %} 8 | 9 | 10 | {% block content %} 11 | 12 | 13 |
14 |
15 |
16 |
17 |

Dockerfile tutorial

18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 |

Level 2 – ENTRYPOINT, USER & EXPOSE

29 |

30 | In this section we will cover three important instructions: ENTRYPOINT, USER and EXPOSE. These commands will help you write more advanced and therefore more complex containers. 31 | At the end of this tutorial you will be able to understand and write this Dockerfile: 32 |

33 |
 34 | # Memcached
 35 | #
 36 | # VERSION       42
 37 | 
 38 | # use the ubuntu base image provided by dotCloud
 39 | FROM ubuntu
 40 | 
 41 | MAINTAINER Victor Coisne victor.coisne@dotcloud.com
 42 | 
 43 | # make sure the package repository is up to date
 44 | RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
 45 | RUN apt-get update
 46 | 
 47 | # install memcached
 48 | RUN apt-get install -y memcached
 49 | 
 50 | # Launch memcached when launching the container
 51 | ENTRYPOINT ["memcached"]
 52 | 
 53 | # run memcached as the daemon user
 54 | USER daemon
 55 | 
 56 | # expose memcached port
 57 | EXPOSE 11211
 58 | 
59 |

60 | Ready? Go!

61 | 62 |

Requirements

63 |

64 | Before taking this tutorial, please make sure that you completed and understand all instructions covered by the Level 1 Dockerfile tutorial (FROM, RUN, MAINTAINER, #). If this is not the case please start with Level 1. 65 |

66 |

67 | The ENTRYPOINT instruction 68 |

69 |

The ENTRYPOINT instruction permits you to trigger a command as soon as the container starts.

70 |

71 | Whale You Be My Container? 72 |

73 |

74 | Let’s say you want your container to execute echo “Whale You Be My Container?” as soon as it starts. Then you will use: 75 |

76 |
 77 | ENTRYPOINT echo “Whale You Be My Container?”
 78 | 
79 |

80 | Let’s try it! 81 |

82 |
 83 | root@precise64:/home/vagrant/dockerfiles/tutorial1# cat Dockerfile_whale_you_be_my_container
 84 | # Whale you be my container?
 85 | #
 86 | # VERSION       0.42
 87 | 
 88 | # use the ubuntu base image provided by dotCloud
 89 | FROM ubuntu
 90 | 
 91 | MAINTAINER Victor Coisne victor.coisne@dotcloud.com
 92 | 
 93 | # say hello when the container is launched
 94 | ENTRYPOINT echo "Whale you be my container"
 95 | root@precise64:/home/vagrant/dockerfiles/tutorial1# docker build -t whaleyoubemycontainer - < Dockerfile_whale_you_be_my_container
 96 | Uploading context 2048 bytes
 97 | Step 1 : FROM ubuntu
 98 | Pulling repository ubuntu
 99 |  ---> b750fe79269dlayersantl) from ubuntu, endpoint: https://cdn-registry-1.docker.io/v1/
100 | Step 2 : MAINTAINER Victor Coisne victor.coisne@dotcloud.com
101 |  ---> Running in 2fd00b89982b
102 |  ---> a2163a4bb4e6
103 | Step 3 : ENTRYPOINT echo "Whale you be my container"
104 |  ---> Running in 81f46dafd81a
105 |  ---> f62c1b30adad
106 | Successfully built f62c1b30adad
107 | root@precise64:/home/vagrant/dockerfiles/tutorial1# docker run whaleyoubemycontainer
108 | Whale you be my container
109 | root@precise64:/home/vagrant/dockerfiles/tutorial1#
110 | 
111 |

112 | There are two syntaxes for the ENTRYPOINT instruction. Instead of command param1 param2 … you could also use the exec format: 113 |

114 |
115 | ENTRYPOINT ["echo", "Whale you be my container"]
116 | 
117 |

118 | This is actually the preferred form, so we will use this form from now on. 119 |

120 |

121 | wc 122 |

123 |

124 | The ENTRYPOINT instruction helps to configure a container that you can run as an executable. That is, when you specify an ENTRYPOINT, the whole container runs as if it was just that executable. Let’s see an example with a a container running wc –l as ENTRYPOINT 125 |

126 |
127 | root@precise64:/home/vagrant/dockerfiles/tutorial1# cat Dockerfile_wc
128 | # This is wc
129 | #
130 | # VERSION       0.42
131 | 
132 | # use the ubuntu base image provided by dotCloud
133 | FROM ubuntu
134 | 
135 | MAINTAINER Victor Coisne victor.coisne@dotcloud.com
136 | 
137 | # count lines with wc
138 | ENTRYPOINT ["wc", "-l"]
139 | root@precise64:/home/vagrant/dockerfiles/tutorial1# docker build -t wc - < Dockerfile_wc
140 | Uploading context 2048 bytes
141 | Step 1 : FROM ubuntu
142 |  ---> b750fe79269d
143 | Step 2 : MAINTAINER Victor Coisne victor.coisne@dotcloud.com
144 |  ---> Using cache
145 |  ---> a2163a4bb4e6
146 | Step 3 : ENTRYPOINT ["wc", "-l"]
147 |  ---> Using cache
148 |  ---> 52504f229d30
149 | Successfully built 52504f229d30
150 | root@precise64:/home/vagrant/dockerfiles/tutorial1# cat /etc/passwd | docker run -i wc
151 | 28
152 | 
153 |

154 | memcached 155 |

156 |

157 | Remember our Dockerfile from Level 1? We can now add an ENTRYPOINT. Memcached needs to be run by the user daemon. So the ENTRYPOINT instruction will look like this: 158 |

159 | ENTRYPOINT ["memcached", "-u", "daemon"] 160 |

161 | This is the resulting Dockerfile 162 |

163 |
164 | # Memcached
165 | #
166 | # VERSION       2.0
167 | 
168 | # use the ubuntu base image provided by dotCloud
169 | FROM ubuntu
170 | MAINTAINER Guillaume J. Charmes guillaume@dotcloud.com
171 | # make sure the package repository is up to date
172 | RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
173 | RUN apt-get update
174 | 
175 | # install memcached
176 | RUN apt-get install -y memcached
177 | 
178 | # Launch memcached when launching the container
179 | ENTRYPOINT ["memcached", "-u", "daemon"]
180 | 
181 |

182 | The USER instruction 183 |

184 |

185 | As we have seen earlier with Memcached, you may need to run the ENTRYPOINT commands as a different user than root. In the previous example, Memcached already has the option to run as another user. But it is not the case for all services/programs. Here comes the USER instruction. As you may have already understood, the USER instruction sets the username or UID to use when running the image. 186 | With this new instruction, instead of this

187 |
188 | ENTRYPOINT ["memcached", "-u", "daemon"]
189 | 
190 |

191 | We can now use this 192 |

193 |
194 | ENTRYPOINT ["memcached"]
195 | USER daemon
196 | 
197 |

198 | Memcached 199 |

200 |

201 | Here is our updated Memcached Dockerfile 202 |

203 |
204 | # Memcached
205 | #
206 | # VERSION       2.1
207 | 
208 | # use the ubuntu base image provided by dotCloud
209 | FROM ubuntu
210 | 
211 | MAINTAINER Victor Coisne victor.coisne@dotcloud.com
212 | 
213 | # make sure the package repository is up to date
214 | RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
215 | RUN apt-get update
216 | 
217 | # install memcached
218 | RUN apt-get install -y memcached
219 | 
220 | # Launch memcached when launching the container
221 | ENTRYPOINT ["memcached"]
222 | 
223 | # run memcached as the daemon user
224 | USER daemon
225 | 
226 |

227 | The EXPOSE instruction 228 |

229 |

230 | The EXPOSE instruction sets ports to be exposed when running the image. 231 |

232 |

233 | Memcached 234 |

235 |

236 | Memcached uses port 11211, so we have to expose this port in order to be able to talk to the Memcached container from outside the container. This is simply done with this line 237 |

238 |
239 | EXPOSE 11211
240 | 
241 |

242 | By doing so, the container will sets the port 11211 to be exposed when running the Memcached image.
243 | The final Dockerfile now looks like this: 244 |

245 |
246 | # Memcached
247 | #
248 | # VERSION       2.2
249 | 
250 | # use the ubuntu base image provided by dotCloud
251 | FROM ubuntu
252 | 
253 | MAINTAINER Victor Coisne victor.coisne@dotcloud.com
254 | 
255 | # make sure the package repository is up to date
256 | RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
257 | RUN apt-get update
258 | 
259 | # install memcached
260 | RUN apt-get install -y memcached
261 | 
262 | # Launch memcached when launching the container
263 | ENTRYPOINT ["memcached"]
264 | 
265 | # run memcached as the daemon user
266 | USER daemon
267 | 
268 | # expose memcached port
269 | EXPOSE 11211
270 | 
271 | 272 |

Playing with our Memcached image 273 |

274 |

275 | So, we are on a server and we need Memcached. Let's use what we have just learned :) 276 |

277 |

278 | Building the image 279 |

280 |

281 | First, let’s build the image, from the Dockerfile. 282 |

283 |
284 | root@precise64:/home/vagrant/dockerfiles/tutorial1# cat Dockerfile
285 | # Memcached
286 | #
287 | # VERSION       2.2
288 | 
289 | # use the ubuntu base image provided by dotCloud
290 | FROM ubuntu
291 | 
292 | MAINTAINER Victor Coisne victor.coisne@dotcloud.com
293 | 
294 | # make sure the package repository is up to date
295 | RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
296 | RUN apt-get update
297 | 
298 | # install memcached
299 | RUN apt-get install -y memcached
300 | 
301 | # Launch memcached when launching the container
302 | ENTRYPOINT ["memcached"]
303 | 
304 | # run memcached as the daemon user
305 | USER daemon
306 | 
307 | # expose memcached port
308 | EXPOSE 11211
309 | root@precise64:/home/vagrant/dockerfiles/tutorial1# docker build -t memcached - < Dockerfile
310 | Uploading context 2560 bytes
311 | Step 1 : FROM ubuntu
312 |  ---> 8dbd9e392a96
313 | Step 2 : MAINTAINER Victor Coisne victor.coisne@dotcloud.com
314 |  ---> Using cache
315 |  ---> dee832c11d42
316 | Step 3 : RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
317 |  ---> Using cache
318 |  ---> 66678b24bab4
319 | Step 4 : RUN apt-get update
320 |  ---> Using cache
321 |  ---> 2fbfe4f542d8
322 | Step 5 : RUN apt-get install -y memcached
323 |  ---> Using cache
324 |  ---> 80c7e70e69a1
325 | Step 6 : ENTRYPOINT ["memcached"]
326 |  ---> Running in df59a42eeaa7
327 |  ---> 090512aab2ac
328 | Step 7 : USER daemon
329 |  ---> Running in f4618f8c72ec
330 |  ---> 264b9e444135
331 | Step 8 : EXPOSE 11211
332 |  ---> Running in e339e4cb6838
333 |  ---> 42a44756989e
334 | Successfully built 42a44756989e
335 | 
336 |

337 | Now let’s launch it. We use the -p flag to publish the port publicly. 338 |

339 | 340 |
root@precise64:/home/vagrant/dockerfiles/tutorial1# docker run -p 11211 memcached
341 | 
342 | 
343 |

344 | Memcached is running :) Now which port should we use to access it from outside the container? 345 | As we asked for it with the EXPOSE instruction, the container is exposing the 11211 port, and we used -p publish it to our system. We can use the docker ps command in order to get the port we should use from outside the container to access Memcached inside the container. 346 |

347 | 348 |
root@precise64:/home/vagrant/dockerfiles/tutorial1# docker ps
349 | ID                  IMAGE               COMMAND             CREATED             STATUS              PORTS                           NAMES
350 | 1aa7a504438c        memcached:latest    memcached           2 minutes ago       Up 2 minutes        0.0.0.0:49153->11211/tcp        blue_whale
351 | 
352 |

353 | In this example the column PORTS shows that we should use the port 49153.
354 | NOTE: you could also use the docker port or the docker inspect commands to retrieve the right port number. 355 |

356 |

357 | Python 358 |

359 |

360 | We will here use python to play with Memcached, but you can use any language (Go, PHP, Ruby, …). This is a simple Python script to use your Memcached (Don’t forget to update the port number, you will probably not have the same). 361 |

362 |
363 | # run this command if import memcache fails
364 | # pip install python-memcached
365 | 
366 | import memcache
367 | 
368 | ip = 'localhost'
369 | port = 49153
370 | 
371 | mc = memcache.Client(["{0}:{1}".format(ip, port)], debug=0)
372 | mc.set("MDM", "Guillaume J. C.")
373 | value = mc.get("MDM")
374 | 
375 | print value
376 | 
377 |

378 | Let’s run it! 379 |

380 |
root@precise64:/home/vagrant/dockerfiles/tutorial2# python test.py
381 | Guillaume J. C.
382 | 
383 |

384 | Congratulations! You just used your first containerized Memcached server :) 385 |

386 | 387 |

Test your Dockerfile skills - Level 2

388 |

Questions

389 |
390 | What is the Dockerfile instruction to specify the base image?
391 | 392 |
393 | 394 | Which Dockerfile instruction sets the default command for your image?
395 | 396 |
397 | 398 | What is the character used to add comments in Dockerfiles?
399 | 400 |
401 | 402 | Which Dockerfile instruction sets the username to use when running the image?
403 | 404 |
405 | 406 | What is the Dockerfile instruction to execute any command on the current image and commit the results?
407 | 408 |
409 | 410 | Which Dockerfile instruction sets ports to be exposed when running the image?
411 | 412 |
413 | 414 | What is the Dockerfile instruction to specify the maintainer of the Dockerfile?
415 | 416 |
417 | 418 | Which Dockerfile instruction lets you trigger a command as soon as the container starts?
419 | 420 |
421 | 422 | 426 | 427 | 428 | 429 |
430 |
431 | 432 |

433 | DockerFill 434 |

435 |

436 | Your best friend Roberto Hashioka sent you a Dockerfile, but some parts were lost in the ocean. Can you find the missing parts? 437 |

438 |
439 | # Redis
440 | #
441 | # VERSION       0.42
442 | 
443 | # use the ubuntu base image provided by dotCloud
444 |   ub
445 | 
446 | MAINT Ro Ha roberto.hashioka@dotcloud.com
447 | 
448 | # make sure the package repository is up to date
449 |  echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
450 |  apt-get update
451 | 
452 | # install wget (required for redis installation)
453 |  apt-get install -y wget
454 | 
455 | # install make (required for redis installation)
456 |  apt-get install -y make
457 | 
458 | # install gcc (required for redis installation)
459 | RUN apt-get install -y 
460 | 
461 | # install apache2
462 |  wget http://download.redis.io/redis-stable.tar.gz
463 | tar xvzf redis-stable.tar.gz
464 | cd redis-stable && make && make install
465 | 
466 | # launch redis when starting the image
467 |  ["redis-server"]
468 | 
469 | # run as user dameon
470 |  daemon
471 | 
472 | # expose port 6379
473 |  6379
474 | 
475 | 476 | 479 | 480 |

481 | 482 |
483 |

What's next?

484 |

485 | We will be posting Level 3 in the next few weeks. Follow us on twitter and/or enter your email address to be alerted!
486 | 487 | 488 |

489 |
490 | 491 | 492 | 493 | Email:

494 | 495 |
496 |

In the meantime, check out this blog post by Michael Crosby that describes Dockerfile Best Practices.

497 | 498 | 499 |
500 |
501 | 502 |
503 |
504 | 505 | 506 | {% endblock %} 507 | -------------------------------------------------------------------------------- /dockerfile_tutorial/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.views.generic import TemplateView 3 | from django.core.urlresolvers import reverse_lazy 4 | from django.views.generic import RedirectView 5 | from django.views.decorators.csrf import ensure_csrf_cookie 6 | 7 | # Uncomment the next two lines to enable the admin: 8 | # from django.contrib import admin 9 | # admin.autodiscover() 10 | 11 | 12 | urlpatterns = patterns('', 13 | # Examples: 14 | 15 | url(r'^$', RedirectView.as_view(url=reverse_lazy('dockerfile'))), 16 | 17 | url(r'^dockerfile/$', ensure_csrf_cookie(TemplateView.as_view(template_name='dockerfile/introduction.html')), name='dockerfile'), 18 | url(r'^dockerfile/level1/$', ensure_csrf_cookie(TemplateView.as_view(template_name='dockerfile/level1.html')), name='dockerfile_level1'), 19 | url(r'^dockerfile/level2/$', ensure_csrf_cookie(TemplateView.as_view(template_name='dockerfile/level2.html')), name='dockerfile_level2'), 20 | 21 | ) 22 | 23 | 24 | -------------------------------------------------------------------------------- /dotcloud.yml: -------------------------------------------------------------------------------- 1 | www: 2 | type: static 3 | 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=1.6,<1.7 2 | South>=0.8,<0.9 3 | 4 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.conf import settings 4 | 5 | 6 | settings.configure( 7 | DEBUG=True, 8 | DATABASES={ 9 | 'default': { 10 | 'ENGINE': 'django.db.backends.sqlite3', 11 | } 12 | }, 13 | ROOT_URLCONF='docker_tutorial.urls', 14 | INSTALLED_APPS=( 15 | 'django.contrib.auth', 16 | 'django.contrib.contenttypes', 17 | 'django.contrib.sessions', 18 | 'django.contrib.admin', 19 | 'docker_tutorial',), 20 | MIDDLEWARE_CLASSES=( 21 | 'django.contrib.sessions.middleware.SessionMiddleware', 22 | )) 23 | 24 | from django.test.runner import DiscoverRunner 25 | test_runner = DiscoverRunner(verbosity=1) 26 | failures = test_runner.run_tests(['docker_tutorial', ]) 27 | if failures: 28 | sys.exit(failures) 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup 3 | from setuptools import find_packages 4 | 5 | setup( 6 | name='docker-tutorial', 7 | version='0.2.1', 8 | author=u'Thatcher Peskens', 9 | author_email='thatcher@dotcloud.com', 10 | packages=find_packages(), 11 | url='https://github.com/dhrp/docker-tutorial/', 12 | license='To be determined', 13 | description='An interactive learning environment to get familiar with the Docker cli', 14 | long_description=open('README.md').read(), 15 | include_package_data=True, 16 | ) 17 | --------------------------------------------------------------------------------